├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── client
├── .env.development
├── package.json
├── public
│ ├── favicon.png
│ ├── index.html
│ └── manifest.json
└── src
│ ├── actions
│ ├── index.js
│ └── types.js
│ ├── components
│ ├── App.js
│ ├── Auth
│ │ ├── NewPassword
│ │ │ ├── NewPassword.js
│ │ │ └── index.js
│ │ ├── Reset
│ │ │ ├── Reset.js
│ │ │ └── index.js
│ │ ├── SignIn
│ │ │ ├── SignIn.css
│ │ │ ├── SignIn.js
│ │ │ ├── SignIn.test.js
│ │ │ └── index.js
│ │ ├── SignOut
│ │ │ ├── SignOut.css
│ │ │ ├── SignOut.js
│ │ │ ├── SignOut.test.js
│ │ │ └── index.js
│ │ ├── SignUp
│ │ │ ├── SignUp.css
│ │ │ ├── SignUp.js
│ │ │ ├── SignUp.test.js
│ │ │ └── index.js
│ │ └── index.js
│ ├── Common
│ │ ├── Footer.js
│ │ ├── Navbar.js
│ │ ├── Pagination.js
│ │ ├── ScrollToTop.js
│ │ ├── SystemMessages.js
│ │ └── ToggleButton.js
│ ├── Features
│ │ ├── Features.js
│ │ ├── Features.scss
│ │ └── index.js
│ ├── Home
│ │ ├── Home.css
│ │ ├── Home.js
│ │ ├── Home.test.js
│ │ └── index.js
│ ├── NoMatch
│ │ ├── NoMatch.css
│ │ ├── NoMatch.js
│ │ ├── NoMatch.test.js
│ │ └── index.js
│ ├── Profile
│ │ ├── Profile.css
│ │ ├── Profile.js
│ │ ├── Profile.test.js
│ │ └── index.js
│ ├── Settings
│ │ ├── Account.js
│ │ ├── ChangePassword.js
│ │ ├── Images.js
│ │ ├── Settings.js
│ │ ├── Settings.scss
│ │ ├── Settings.test.js
│ │ └── index.js
│ ├── StaticPages
│ │ ├── PrivacyPolicy.js
│ │ ├── StaticPages.scss
│ │ └── TermsOfService.js
│ ├── User
│ │ ├── Edit.js
│ │ ├── User.css
│ │ ├── User.js
│ │ ├── User.test.js
│ │ └── index.js
│ ├── Users
│ │ ├── Edit.js
│ │ ├── List.js
│ │ ├── Users.js
│ │ ├── Users.scss
│ │ └── index.js
│ └── Welcome
│ │ ├── Welcome.js
│ │ ├── Welcome.scss
│ │ ├── Welcome.test.js
│ │ └── index.js
│ ├── history.js
│ ├── index.js
│ ├── reducers
│ ├── auth.js
│ ├── index.js
│ ├── settings.js
│ └── system.js
│ ├── routes
│ └── index.js
│ ├── serviceWorker.js
│ └── styles
│ ├── App.scss
│ ├── _forms.scss
│ ├── _global.scss
│ ├── _intro.scss
│ ├── _keyframes.scss
│ ├── _main.scss
│ ├── _mixins.scss
│ ├── _navbar.scss
│ ├── _profile.scss
│ ├── _sidebar.scss
│ ├── _variables.scss
│ └── img
│ ├── intro-header-bg.jpg
│ └── logo.svg
├── common
├── mixins
│ ├── FullName.js
│ ├── Tags.js
│ └── TimeStamp.js
└── models
│ ├── file.js
│ ├── file.json
│ ├── user.js
│ └── user.json
├── data
└── README.md
├── package.json
└── server
├── boot
├── autentication.js
├── init.js
└── root.js
├── component-config.json
├── config.json
├── datasources.json
├── middleware.development.json
├── middleware.json
├── model-config.json
├── models
├── container.js
└── container.json
└── server.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 2017
4 | },
5 |
6 | "env": {
7 | "es6": true
8 | }
9 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.csv
2 | *.dat
3 | *.iml
4 | *.log
5 | *.out
6 | *.pid
7 | *.seed
8 | *.sublime-*
9 | *.swo
10 | *.swp
11 | *.tgz
12 | *.xml
13 | *.lock
14 | .DS_Store
15 | .idea
16 | .project
17 | .strong-pm
18 | coverage
19 | node_modules
20 | npm-debug.log
21 | package-lock.json
22 | client/build
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 goker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # loopback-react playground
2 | loopback (mongodb) - react, router, redux, axios, formik, bootstrap 4 (reactstrap) fullstack playground with authentication and user management
3 |
4 | This a full stack playground for developing a Loopback - React application. And it's _under the development_ still.
5 |
6 | For more info you can check:
7 | - https://loopback.io/doc/en/lb3/index.html
8 | - https://github.com/facebook/create-react-app
9 | - https://reacttraining.com/react-router/web/guides/philosophy
10 | - https://redux.js.org/
11 | - https://github.com/jaredpalmer/formik
12 | - https://getbootstrap.com/docs/4.1/getting-started/introduction/
13 | - https://reactstrap.github.io/
14 |
15 | ### Install
16 | ```
17 | yarn install && cd client && yarn install
18 |
19 | ```
20 |
21 | ### Run Loopback
22 | ```
23 | yarn start
24 | ```
25 | The server run on **localhost:3003** and **server/boot/init.js** will create three users _(admin, editor and user)_
26 |
27 | You can reach the Api Explorer via **localhost:3003/explorer**
28 |
29 | ### Run Client (React)
30 | ```
31 | cd client && yarn start
32 | ```
33 | The client run on **localhost:3000** and talking with api on **localhost:3003**
34 |
35 |
36 | ### Run for Development
37 | ```
38 | yarn watch
39 | ```
40 | The watch command is running loopback at background. If you need to kill both services running on 3000 and 3003, you can use
41 | ```
42 | yarn kill
43 | ```
44 |
45 |
46 | ##### Add a React Component
47 | ```
48 | cd client/
49 | npx crcf src/components/NewComponent
50 | ```
51 |
52 | #### Build and Serve the Client
53 | ```
54 | cd client/
55 | yarn build
56 | ```
57 | After the build to serve the client you should edit the **server/middleware.json** like below
58 | ```
59 | "files": {
60 | "loopback#static": [
61 | {
62 | "paths": ["/"],
63 | "params": "$!../client/build"
64 | },
65 | {
66 | "paths": ["*"],
67 | "params": "$!../client/build"
68 | }
69 | ]
70 | }
71 | ```
72 | to more info https://loopback.io/doc/en/lb3/Defining-middleware.html
73 |
74 | ## Routes
75 | ```
76 | /
77 | /feature
78 | /signin
79 | /signup
80 | /signout
81 | /reset
82 | /newpassword/:token
83 | /tos
84 | /privacy
85 | - /home
86 | /profile
87 | /settings/:page? (default /settings/account)
88 | /users/:page?/:id? (default /users/list)
89 | /user/:id
90 | /user/:id/edit
91 | /:username (run as /profile)
92 | ```
93 | in client/src/routes/index.js
94 |
--------------------------------------------------------------------------------
/client/.env.development:
--------------------------------------------------------------------------------
1 | SKIP_PREFLIGHT_CHECK=true
2 | REACT_APP_API_URL=http://localhost:3003/api
3 | REACT_APP_FILE_URL=http://localhost:3003
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "loopback-react-playground",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.19.0",
7 | "bootstrap": "^4.3.1",
8 | "form-serialize": "^0.7.2",
9 | "formik": "^1.5.8",
10 | "history": "^4.9.0",
11 | "moment": "^2.24.0",
12 | "node-sass": "^4.12.0",
13 | "nprogress": "^0.2.0",
14 | "prop-types": "^15.7.2",
15 | "qs": "^6.7.0",
16 | "react": "^16.8.6",
17 | "react-check-auth": "^0.2.0-alpha.2",
18 | "react-dom": "^16.8.6",
19 | "react-number-format": "^4.0.8",
20 | "react-redux": "^7.1.0",
21 | "react-router": "^5.0.1",
22 | "react-router-dom": "^5.0.1",
23 | "react-scripts": "^3.0.1",
24 | "reactstrap": "^8.0.1",
25 | "reactstrap-typeahead": "^1.0.1",
26 | "redux": "^4.0.4",
27 | "redux-promise": "^0.6.0",
28 | "redux-thunk": "^2.3.0",
29 | "yup": "^0.27.0"
30 | },
31 | "scripts": {
32 | "start": "react-scripts start",
33 | "build": "react-scripts build",
34 | "test": "react-scripts test --env=jsdom",
35 | "eject": "react-scripts eject"
36 | },
37 | "eslintConfig": {
38 | "extends": "react-app"
39 | },
40 | "browserslist": [
41 | ">0.2%",
42 | "not dead",
43 | "not ie <= 11",
44 | "not op_mini all"
45 | ],
46 | "devDependencies": {
47 | "create-react-component-folder": "^0.1.30"
48 | },
49 | "crcf": [
50 | "scss"
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/client/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/public/favicon.png
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | KodKafa | React-Loopback Full-stack Playground
10 |
13 |
14 |
15 |
17 |
19 |
20 |
21 |
22 | You need to enable JavaScript to run this app.
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.png",
7 | "sizes": "128x128",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import Qs from 'qs';
2 | import axios from 'axios';
3 | import History from '../history.js';
4 | import * as type from './types';
5 |
6 | const API_URL = process.env.REACT_APP_API_URL;
7 | const FILE_URL = process.env.REACT_APP_FILE_URL;
8 | let TOKEN = localStorage.getItem('token');
9 | let UID = localStorage.getItem('uid');
10 |
11 | const errorBeautifier = error => {
12 | return error.hasOwnProperty('payload')
13 | ? error
14 | : {type: type.ERROR, payload: {data: {error: {name: 'Error', message: error.message}}}}
15 | };
16 |
17 | axios.interceptors.response.use(
18 | response => response,
19 | error => Promise.reject(error && error.response
20 | ? {type: type.ERROR, payload: error.response}
21 | : errorBeautifier(error))
22 | );
23 | axios.interceptors.request.use(config => {
24 | console.log(config);
25 | config.paramsSerializer = params => {
26 | // Qs is already included in the Axios package
27 | return Qs.stringify(params, {
28 | arrayFormat: "brackets",
29 | encode: false
30 | });
31 | };
32 | return config;
33 | });
34 |
35 | export const signUp = (data) => {
36 | return (dispatch) => {
37 | axios.post(`${API_URL}/users`, data)
38 | .then(() => {
39 | dispatch({
40 | type: type.SUCCESS,
41 | payload: {name: 'SUCCESS', message: 'Check your email for confirmation link.'}
42 | });
43 | setTimeout(() => {
44 | History.push('/signin')
45 | }, 1500);
46 | })
47 | .catch(error => dispatch(error));
48 | };
49 | };
50 |
51 | export const setSession = (response) => {
52 | //response.isAdmin = response.roles.find(x => x.name === 'admin');
53 | //response.isEditor = response.roles.find(x => x.name === 'editor');
54 | sessionStorage.setItem('me', JSON.stringify(response));
55 | return response;
56 | };
57 |
58 | export const signIn = (data) => {
59 | return async (dispatch) => {
60 | await axios.post(`${API_URL}/users/login`, data)
61 | .then(response => {
62 | localStorage.setItem('token', response.data.id);
63 | localStorage.setItem('uid', response.data.userId);
64 | localStorage.setItem('ttl', response.data.ttl);
65 | TOKEN = localStorage.getItem('token');
66 | UID = localStorage.getItem('uid');
67 | getUser(response.data.userId)
68 | .then(response => {
69 | dispatch({type: type.AUTH_USER, payload: setSession(response)});
70 | History.push('/home');
71 | })
72 | .catch(error => dispatch(error));
73 | })
74 | .catch(error => dispatch(error));
75 | };
76 | };
77 |
78 | export const signOut = () => {
79 | return (dispatch) => {
80 | localStorage.removeItem('token');
81 | sessionStorage.removeItem('me');
82 | TOKEN = null;
83 | dispatch({
84 | type: type.UNAUTH_USER,
85 | payload: null
86 | });
87 | History.push('/');
88 | }
89 | };
90 |
91 | export const resetPasswordRequest = (email) => {
92 | return (dispatch) => {
93 | axios.post(`${API_URL}/users/reset`, {email})
94 | .then(() => {
95 | dispatch({
96 | type: type.SUCCESS,
97 | payload: {name: 'SUCCESS', message: 'We sent an email to you. Please check your email.'}
98 | })
99 | })
100 | .catch(error => dispatch(error));
101 | };
102 | };
103 |
104 | export const resetPassword = ({token, password}) => {
105 | return (dispatch) => {
106 | axios.post(`${API_URL}/users/reset-password`, {newPassword: password}, {
107 | headers: {authorization: token}
108 | })
109 | .then(() => {
110 | dispatch({
111 | type: type.SUCCESS,
112 | payload: {name: 'SUCCESS', message: 'You has been changed your password successfully.'}
113 | });
114 | setTimeout(() => {
115 | History.push('/signin')
116 | }, 1500);
117 | })
118 | .catch(error => dispatch(error));
119 | };
120 | };
121 |
122 | function computeUser(user) {
123 | user = user || {roles: []};
124 | user.isAdmin = user.roles.find(x => x.name === 'admin');
125 | user.isEditor = user.roles.find(x => x.name === 'editor');
126 | user.isManager = user.roles.find(x => x.name === 'manager');
127 | user.isWorker = user.roles.find(x => x.name === 'worker');
128 | user.icon = user.isAdmin ? 'fas fa-user-astronaut'
129 | : user.isEditor ? 'fa fa-user-secret'
130 | : user.isManager || user.isWorker ? 'fa fa-user-tie' : 'fa fa-user';
131 | if (typeof user.image === 'object') {
132 | user.image.thumb = FILE_URL + user.image.normal;
133 | user.image.normal = FILE_URL + user.image.normal;
134 | user.image.url = FILE_URL + user.image.url;
135 | } else {
136 | user.image = {
137 | thumb: 'http://holder.ninja/50x50,P.svg',
138 | normal: 'http://holder.ninja/250x250,PROFILE.svg',
139 | url: 'http://holder.ninja/500x500,PROFILE.svg'
140 | };
141 | }
142 | if (typeof user.cover === 'object') {
143 | user.cover.thumb = FILE_URL + user.cover.normal;
144 | user.cover.normal = FILE_URL + user.cover.normal;
145 | user.cover.url = FILE_URL + user.cover.url;
146 | } else {
147 | user.cover = {
148 | thumb: 'http://holder.ninja/400x120,COVER-1200x360.svg',
149 | normal: 'http://holder.ninja/1200x360,COVER-1200x360.svg',
150 | url: 'http://holder.ninja/1200x360,COVER-1200x360.svg'
151 | };
152 | }
153 | return user;
154 | }
155 |
156 | export const getUser = (uid) => {
157 | return new Promise((resolve, reject) => {
158 | axios.get(`${API_URL}/users/${uid}`, {
159 | headers: {authorization: localStorage.getItem('token')}
160 | })
161 | .then(response => {
162 | resolve(computeUser(response.data))
163 | })
164 | .catch(error => {
165 | reject(error.response && error.response.data && error.response.data.error)
166 | });
167 | });
168 | };
169 |
170 | export const fetchUser = (id) => {
171 | return (dispatch) => {
172 | axios.get(`${API_URL}/users/${id}`, {
173 | headers: {authorization: localStorage.getItem('token')}
174 | })
175 | .then(response => {
176 | dispatch({type: type.DATA, payload: computeUser(response.data)});
177 | })
178 | .catch(error => dispatch(error));
179 | }
180 | };
181 |
182 | export const searchUserByName = async (name) => {
183 | const query = JSON.stringify({where: {name: {like: name, options: 'i'}}});
184 | try {
185 | const response = await axios.get(`${API_URL}/users?filter=${query}`, {
186 | headers: {authorization: localStorage.getItem('token')}
187 | });
188 | console.log('searchUserByName', response);
189 | return response.data
190 | } catch (error) {
191 | throw error
192 | }
193 | };
194 |
195 | export const getProfile = (username) => {
196 | return (dispatch) => {
197 | axios.get(`${API_URL}/users/profile/${username}`, {
198 | headers: {authorization: localStorage.getItem('token')}
199 | })
200 | .then(response => {
201 | dispatch({type: type.DATA, payload: computeUser(response.data.user)});
202 | })
203 | .catch(error => {
204 | dispatch(errorBeautifier(error))
205 | });
206 | }
207 |
208 | // return new Promise((resolve, reject) => {
209 | // axios.get(`${API_URL}/users/profile/${username}`, {
210 | // headers: {authorization: localStorage.getItem('token')}
211 | // })
212 | // .then(response => {
213 | // resolve(computeUser(response.data.user))
214 | // })
215 | // .catch(error => {
216 | // reject(error.response.data.error)
217 | // });
218 | // });
219 | };
220 |
221 | export const getSession = (callback) => {
222 | const token = localStorage.getItem('token');
223 | const me = sessionStorage.getItem('me');
224 | if (token) {
225 | if (me) {
226 | callback(JSON.parse(me));
227 | } else {
228 | getUser(localStorage.getItem('uid'))
229 | .then((response) => {
230 | callback(setSession(response))
231 | })
232 | .catch((error) => {
233 | localStorage.removeItem('token');
234 | localStorage.removeItem('uid');
235 | localStorage.removeItem('ttl');
236 | window.location.reload(true);
237 | throw error;
238 | });
239 | }
240 | } else callback(null)
241 | };
242 |
243 | export const addUser = (data) => {
244 | return (dispatch) => {
245 | axios.post(`${API_URL}/users`, data)
246 | .then(() => {
247 | History.push('/users');
248 | })
249 | .catch(error => dispatch(error));
250 | };
251 | };
252 |
253 | export const settingsAccount = (data) => {
254 | return (dispatch) => {
255 | axios.patch(`${API_URL}/users/${UID}`, data,
256 | {headers: {authorization: TOKEN}})
257 | .then(response => {
258 | dispatch({type: type.AUTH_USER, payload: setSession(computeUser(response.data))});
259 | dispatch({
260 | type: type.SUCCESS,
261 | payload: {name: 'SUCCESS', message: 'Your account has been updated successfully!'}
262 | });
263 | })
264 | .catch(error => dispatch(error));
265 | };
266 | };
267 |
268 | export const settingsChangePassword = ({oldPassword, newPassword}) => {
269 | return (dispatch) => {
270 | axios.post(`${API_URL}/users/change-password`, {oldPassword, newPassword},
271 | {headers: {authorization: localStorage.getItem('token')}})
272 | .then(() => {
273 | dispatch({
274 | type: type.SUCCESS,
275 | payload: {name: 'SUCCESS', message: 'Your password has been changed successfully!'}
276 | });
277 | })
278 | .catch(error => dispatch(error));
279 | }
280 | };
281 |
282 | export const getUsers = () => {
283 | return (dispatch) => {
284 | axios.get(`${API_URL}/users`, {
285 | headers: {authorization: localStorage.getItem('token')}
286 | })
287 | .then(response => {
288 | dispatch({type: type.DATA, payload: response.data.map(user => computeUser(user))});
289 | })
290 | .catch(error => dispatch(error));
291 | }
292 | };
293 |
294 | export const fetchUsers = (params = {}) => {
295 | return (dispatch) => {
296 | axios.get(`${API_URL}/users/list`, {
297 | headers: {authorization: localStorage.getItem('token')},
298 | params
299 | })
300 | .then(response => {
301 | response.data.list.items.map(user => computeUser(user));
302 | dispatch({type: type.DATA, payload: response.data.list});
303 | })
304 | .catch(error => dispatch(error));
305 | }
306 | };
307 |
308 | export const updateUser = async (data) => {
309 | await axios.patch(`${API_URL}/users/${data.id}`, data,
310 | {headers: {authorization: TOKEN}})
311 | .then(response => {
312 | return response.data
313 | })
314 | .catch(error => {
315 | throw error.response.data.error.message;
316 | });
317 | };
318 |
319 | export const toggleAdmin = (id, toggleType = 'Admin') => {
320 | return (dispatch) => {
321 | axios.post(`${API_URL}/users/${id}/toggle${toggleType}`, {id},
322 | {headers: {authorization: TOKEN}})
323 | .then(response => {
324 | dispatch({type: type.DATA, payload: computeUser(response.data.data)});
325 | })
326 | .catch(error => {
327 | dispatch({
328 | type: type.ERROR,
329 | payload: error.response
330 | });
331 | });
332 | }
333 | };
334 |
335 | export const toggleEditor = (id) => {
336 | return toggleAdmin(id, 'Editor')
337 | };
338 |
339 | export const toggleManager = (id) => {
340 | return toggleAdmin(id, 'Manager')
341 | };
342 |
343 | export const toggleWorker = (id) => {
344 | return toggleAdmin(id, 'Worker')
345 | };
346 |
347 | export const toggleStatus = (id) => {
348 | return toggleAdmin(id, 'Status')
349 | };
350 |
351 | export const uploadCoverImage = (file) => {
352 | return async (dispatch) => {
353 | await axios.post(`${API_URL}/users/${UID}/cover`, file, {
354 | headers: {authorization: TOKEN},
355 | onUploadProgress: progressEvent => {
356 | console.log(progressEvent.loaded, progressEvent.total)
357 | }
358 | })
359 | .then(response => {
360 | dispatch({type: type.AUTH_USER, payload: setSession(computeUser(response.data.user))});
361 | })
362 | .catch(error => dispatch(error));
363 | };
364 | };
365 |
366 | export const uploadProfileImage = (file) => {
367 | return async (dispatch) => {
368 | await axios.post(`${API_URL}/users/${UID}/image`, file, {
369 | headers: {authorization: TOKEN},
370 | onUploadProgress: progressEvent => {
371 | console.log(progressEvent.loaded, progressEvent.total)
372 | }
373 | })
374 | .then(response => {
375 | dispatch({type: type.AUTH_USER, payload: setSession(computeUser(response.data.user))});
376 | })
377 | .catch(error => dispatch(error));
378 | };
379 | };
380 |
381 |
--------------------------------------------------------------------------------
/client/src/actions/types.js:
--------------------------------------------------------------------------------
1 | export const ERROR = 'error';
2 | export const SUCCESS = 'success';
3 | export const DATA = 'data';
4 | export const FILE = 'file';
5 |
6 | export const AUTH_USER = 'auth_user';
7 | export const UNAUTH_USER = 'unauth_user';
8 | export const AUTH_ERROR = 'auth_error';
9 | export const CHANGEPASSWORD_ERROR = 'changepassword_error';
10 | export const CHANGEPASSWORD_SUCCESS = 'changepassword_success';
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import Navbar from './Common/Navbar'
3 | import Footer from './Common/Footer'
4 |
5 | import '../styles/App.scss';
6 |
7 | class App extends PureComponent {
8 | constructor(props) {
9 | super(props)
10 | this.state = {
11 | me: props.me
12 | }
13 | }
14 |
15 | render() {
16 | console.log('App render', this.state.me);
17 | return (
18 |
19 |
20 |
21 | {this.props.children}
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | export default App;
30 |
--------------------------------------------------------------------------------
/client/src/components/Auth/NewPassword/NewPassword.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Field, Form, withFormik} from 'formik'
3 | import * as Yup from 'yup'
4 | import {resetPassword} from '../../../actions'
5 | import {connect} from 'react-redux'
6 |
7 | let setSubmittingHigher;
8 |
9 | const FormikForm = ({
10 | values,
11 | touched,
12 | errors,
13 | isSubmitting
14 | }) => (
15 |
42 | );
43 |
44 | const EnhancedForm = withFormik({
45 | mapPropsToValues({match}) {
46 | return {
47 | token: match.params.token,
48 | password: ''
49 | }
50 | },
51 | validationSchema: Yup.object().shape({
52 | token: Yup.string().required('Token is required'),
53 | password: Yup.string().required('Password is required').min(8, 'Password must be 8 characters or longer')
54 | .matches(/[a-z]/, 'Password must contain at least one lowercase char')
55 | .matches(/[A-Z]/, 'Password must contain at least one uppercase char')
56 | .matches(/[a-zA-Z]+[^a-zA-Z\s]+/, 'at least 1 number or special char (@,!,#, etc).'),
57 | }),
58 | handleSubmit(values, {props, resetForm, setSubmitting}) {
59 | setSubmittingHigher = (success) => {
60 | setSubmitting();
61 | if (success) {
62 | resetForm();
63 | }
64 | };
65 | props.resetPassword(values);
66 | }
67 | })(FormikForm);
68 |
69 | const mapStateToProps = (state) => {
70 | typeof setSubmittingHigher === 'function' && setSubmittingHigher(!!state.system.message);
71 | return {system: state.system}
72 | };
73 |
74 | const mapDispatchToProps = (dispatch) => {
75 | return {
76 | resetPassword: (values) => {
77 | dispatch(resetPassword(values));
78 | },
79 | }
80 | };
81 |
82 | export default connect(mapStateToProps, mapDispatchToProps)(EnhancedForm);
83 |
--------------------------------------------------------------------------------
/client/src/components/Auth/NewPassword/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './NewPassword';
2 |
--------------------------------------------------------------------------------
/client/src/components/Auth/Reset/Reset.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router-dom';
3 | import {Field, Form, withFormik} from 'formik'
4 | import * as Yup from 'yup'
5 | import {resetPasswordRequest} from '../../../actions'
6 | import {connect} from 'react-redux'
7 |
8 | let setSubmittingHigher;
9 |
10 | const FormikForm = ({
11 | values,
12 | touched,
13 | errors,
14 | isSubmitting
15 | }) => (
16 |
38 | );
39 |
40 | const EnhancedForm = withFormik({
41 | mapPropsToValues() {
42 | return {
43 | email: ''
44 | }
45 | },
46 | validationSchema: Yup.object().shape({
47 | email: Yup.string().email('Please write a correct email address').required('Email is required'),
48 | }),
49 | handleSubmit(values, {props, setSubmitting, resetForm}) {
50 | setSubmittingHigher = () => {
51 | setSubmitting();
52 | resetForm();
53 | };
54 | props.resetPasswordRequest(values.email)
55 | }
56 | })(FormikForm);
57 |
58 | const mapStateToProps = (state) => {
59 | typeof setSubmittingHigher === 'function' && setSubmittingHigher(false);
60 | return {system: state.system}
61 | };
62 |
63 | const mapDispatchToProps = (dispatch) => {
64 | return {
65 | resetPasswordRequest: (values) => {
66 | dispatch(resetPasswordRequest(values));
67 | },
68 | }
69 | };
70 |
71 | export default connect(mapStateToProps, mapDispatchToProps)(EnhancedForm);
72 |
--------------------------------------------------------------------------------
/client/src/components/Auth/Reset/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Reset';
2 |
--------------------------------------------------------------------------------
/client/src/components/Auth/SignIn/SignIn.css:
--------------------------------------------------------------------------------
1 | @media all and (min-width: 480px) {
2 | .Login {
3 | padding: 60px 0;
4 | }
5 |
6 | .Login form {
7 | margin: 0 auto;
8 | max-width: 320px;
9 | }
10 | }
--------------------------------------------------------------------------------
/client/src/components/Auth/SignIn/SignIn.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router-dom';
3 | import {Field, Form, withFormik} from 'formik'
4 | import * as Yup from 'yup'
5 | import {signIn} from '../../../actions'
6 | import {connect} from 'react-redux'
7 |
8 | let setSubmittingHigher;
9 | const FormikForm = ({
10 | values,
11 | touched,
12 | errors,
13 | isSubmitting
14 | }) => (
15 |
45 | );
46 |
47 | const EnhancedForm = withFormik({
48 | mapPropsToValues({username}) {
49 | return {
50 | username: username || '',
51 | password: ''
52 | }
53 | },
54 | validationSchema: Yup.object().shape({
55 | username: Yup.string().required('Email/username is required'),
56 | // password: Yup.string().min(8, 'Password must be 8 characters or longer')
57 | // .matches(/[a-z]/, 'Password must contain at least one lowercase char')
58 | // .matches(/[A-Z]/, 'Password must contain at least one uppercase char')
59 | // .matches(/[a-zA-Z]+[^a-zA-Z\s]+/, 'at least 1 number or special char (@,!,#, etc).'),
60 | }),
61 | handleSubmit(values, {props, resetForm, setFieldError, setSubmitting}) {
62 | if (values.username.match(/^[A-z0-9._%+-]+@[A-z0-9.-]+\.[A-z0-9.]{2,}$/))
63 | values = {email: values.username, password: values.password};
64 | setSubmittingHigher = setSubmitting;
65 | props.signIn(values);
66 | },
67 | })(FormikForm);
68 |
69 | const mapStateToProps = (state) => {
70 | typeof setSubmittingHigher === 'function' && setSubmittingHigher(false);
71 | return {system: state.system}
72 | };
73 |
74 | const mapDispatchToProps = (dispatch) => {
75 | return {
76 | signIn: (values) => {
77 | dispatch(signIn(values));
78 | },
79 | }
80 | };
81 |
82 | export default connect(mapStateToProps, mapDispatchToProps)(EnhancedForm);
83 |
--------------------------------------------------------------------------------
/client/src/components/Auth/SignIn/SignIn.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import SignIn from './SignIn';
4 |
5 | describe(' ', () => {
6 | test('renders', () => {
7 | const wrapper = shallow( );
8 | expect(wrapper).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/components/Auth/SignIn/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './SignIn';
2 |
--------------------------------------------------------------------------------
/client/src/components/Auth/SignOut/SignOut.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/components/Auth/SignOut/SignOut.css
--------------------------------------------------------------------------------
/client/src/components/Auth/SignOut/SignOut.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import {connect} from 'react-redux';
3 | import * as actions from '../../../actions';
4 |
5 | class SignOut extends PureComponent {
6 | componentWillMount() {
7 | this.props.signOut();
8 | }
9 |
10 | render() {
11 | return Sorry to see you go ...
;
12 | }
13 | }
14 |
15 | export default connect(null, actions)(SignOut);
16 |
--------------------------------------------------------------------------------
/client/src/components/Auth/SignOut/SignOut.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import SignOut from './SignOut';
4 |
5 | describe(' ', () => {
6 | test('renders', () => {
7 | const wrapper = shallow( );
8 | expect(wrapper).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/components/Auth/SignOut/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './SignOut';
2 |
--------------------------------------------------------------------------------
/client/src/components/Auth/SignUp/SignUp.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/components/Auth/SignUp/SignUp.css
--------------------------------------------------------------------------------
/client/src/components/Auth/SignUp/SignUp.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router-dom';
3 | import {Field, Form, withFormik} from 'formik'
4 | import * as Yup from 'yup'
5 | import {signUp} from '../../../actions'
6 | import {connect} from 'react-redux'
7 |
8 | const FormikForm = ({
9 | values,
10 | touched,
11 | errors,
12 | status,
13 | isSubmitting
14 | }) => (
74 | );
75 |
76 | const EnhancedForm = withFormik({
77 | mapPropsToValues() {
78 | return {
79 | username: '',
80 | email: '',
81 | name: '',
82 | surname: '',
83 | password: '',
84 | }
85 | },
86 | validationSchema: Yup.object().shape({
87 | username: Yup.string().matches(/^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$/, 'Username only contain english characters and (_,-,.). Also usernames must start and end with a letter or number.')
88 | .required('Username is required'),
89 | email: Yup.string().email('Please write a correct email address').required('Email is required'),
90 | name: Yup.string().required('Name is required'),
91 | surname: Yup.string().required('Surname is required'),
92 | password: Yup.string().min(8, 'Password must be 8 characters or longer')
93 | .matches(/[a-z]/, 'Password must contain at least one lowercase char')
94 | .matches(/[A-Z]/, 'Password must contain at least one uppercase char')
95 | .matches(/[a-zA-Z]+[^a-zA-Z\s]+/, 'at least 1 number or special char (@,!,#, etc).'),
96 | }),
97 | async handleSubmit(values, {props, resetForm, setFieldError, setSubmitting, setStatus}) {
98 | setStatus(null);
99 | try {
100 | await props.signUp(values);
101 | //setStatus({'success': 'Your account has been created successfully!'})
102 | setSubmitting(false);
103 | //resetForm();
104 | } catch (errors) {
105 | //setStatus({'error': errors})
106 | //setSubmitting(false);
107 | }
108 | }
109 | })(FormikForm);
110 |
111 | const mapStateToProps = (state) => {
112 | return {me: state.auth.me}
113 | };
114 |
115 | const mapDispatchToProps = (dispatch) => {
116 | return {
117 | signUp: (values) => {
118 | dispatch(signUp(values));
119 | },
120 | }
121 | };
122 |
123 | export default connect(mapStateToProps, mapDispatchToProps)(EnhancedForm);
124 |
--------------------------------------------------------------------------------
/client/src/components/Auth/SignUp/SignUp.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import SignUp from './SignUp';
4 |
5 | describe(' ', () => {
6 | test('renders', () => {
7 | const wrapper = shallow( );
8 | expect(wrapper).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/components/Auth/SignUp/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './SignUp';
2 |
--------------------------------------------------------------------------------
/client/src/components/Auth/index.js:
--------------------------------------------------------------------------------
1 | import AuthProvider from './AuthProvider';
2 | import AuthConsumer from './AuthConsumer';
3 |
4 | export {AuthProvider, AuthConsumer};
5 |
--------------------------------------------------------------------------------
/client/src/components/Common/Footer.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import {Link} from 'react-router-dom';
3 |
4 | class Footer extends PureComponent {
5 | constructor(props) {
6 | super(props)
7 | this.state = {}
8 | }
9 |
10 | render() {
11 | return (
12 |
39 | );
40 | }
41 | }
42 |
43 | export default Footer;
44 |
--------------------------------------------------------------------------------
/client/src/components/Common/Navbar.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import {connect} from 'react-redux';
3 | import {Link} from 'react-router-dom';
4 | import SystemMessages from './SystemMessages';
5 | import logo from '../../styles/img/logo.svg';
6 |
7 | class Navbar extends PureComponent {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | me: props.me,
12 | shrink: !!props.me
13 | };
14 | }
15 |
16 | static dropDownToggle(e) {
17 | const dd = e.currentTarget;
18 | dd.classList.toggle('show');
19 | dd.querySelector('.dropdown-menu').classList.toggle('show');
20 | }
21 |
22 | static Shrink(e, shrink) {
23 | const navbar = document.getElementById('navbar');
24 | if (shrink || window.pageYOffset > 1) {
25 | navbar.classList.add("navbar-shrink");
26 | } else {
27 | navbar.classList.remove("navbar-shrink");
28 | }
29 | }
30 |
31 | componentDidMount() {
32 | Navbar.Shrink(null, this.state.shrink);
33 | if (!this.state.shrink)
34 | window.addEventListener('scroll', Navbar.Shrink);
35 | }
36 |
37 | componentDidUpdate(props) {
38 | this.setState({
39 | me: props.me,
40 | shrink: !!props.me
41 | });
42 | Navbar.Shrink(null, props.me);
43 | if (props.me)
44 | window.removeEventListener('scroll', Navbar.Shrink);
45 | else
46 | window.addEventListener('scroll', Navbar.Shrink);
47 | }
48 |
49 |
50 | renderLogo() {
51 | return
52 |
53 |
54 | }
55 |
56 | renderLinks() {
57 | if (this.state.me) {
58 | return
59 |
60 | Users
61 |
62 | {this.state.me.isWorker &&
63 |
64 | Sales Slips
65 | }
66 |
67 | } else {
68 | return [
69 | Features
70 | ];
71 | }
72 | }
73 |
74 | renderUserMenu() {
75 | if (this.state.me && this.state.me.image) {
76 | return [
77 |
78 |
79 |
81 | {this.state.me.name}
82 |
83 |
84 |
Profile
85 |
Settings
86 |
87 |
Log out
88 |
89 |
90 | ];
91 | } else {
92 | return [
93 |
94 | Sign In
95 | ,
96 |
97 | Sign Up
98 |
99 | ];
100 | }
101 | }
102 |
103 | render() {
104 | return (
105 |
106 |
107 | {this.renderLogo()}
108 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | About
117 |
118 | {this.renderLinks()}
119 |
120 |
121 |
122 | {this.renderUserMenu()}
123 |
124 |
125 |
126 |
127 |
128 | );
129 | }
130 | }
131 |
132 | const mapStateToProps = (state) => {
133 | return {me: state.auth.me}
134 | }
135 |
136 | export default connect(mapStateToProps)(Navbar);
137 |
--------------------------------------------------------------------------------
/client/src/components/Common/Pagination.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link} from 'react-router-dom';
3 | import {Pagination, PaginationItem} from 'reactstrap';
4 |
5 | class CustomPagination extends React.Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | const totalPages = Math.ceil(props.count / props.limit);
10 | const range = Math.min(totalPages, props.range);
11 | const center = totalPages > range ? Math.ceil(range / 2) : range;
12 | const start = (props.page > center ? (props.page >= totalPages - center ? totalPages - range : props.page - center) : 1);
13 | const end = totalPages > range ? (start + range) : (start + range - 1);
14 | this.state = {
15 | range,
16 | center,
17 | start,
18 | end,
19 | page: props.page,
20 | query: props.query,
21 | totalPages
22 | };
23 | }
24 |
25 | pageNumbers = () => {
26 | let pages = [];
27 | for (let i = this.state.start; i <= this.state.end; i++) {
28 | const query = new URLSearchParams({...this.state.query, page: i});
29 | const isActive = i === parseInt(this.state.page);
30 | pages.push(
31 | {i}
32 | )
33 | }
34 | return pages
35 | };
36 |
37 | componentDidUpdate = (props) => {
38 | const totalPages = Math.ceil(props.count / props.limit);
39 | const range = Math.min(totalPages, props.range);
40 | const center = totalPages > range ? Math.ceil(range / 2) : range;
41 | const start = (props.page > center ? (props.page >= totalPages - center ? totalPages - range : props.page - center) : 1);
42 | const end = totalPages > range ? (start + range) : (start + range - 1);
43 | this.setState({
44 | range,
45 | center,
46 | start,
47 | end,
48 | page: props.page,
49 | query: props.query,
50 | totalPages
51 | });
52 | };
53 |
54 | render() {
55 | const className = this.state.status ? this.state.classActive : this.state.classPassive;
56 | const first = '?' + (new URLSearchParams({...this.state.query, page: 1})).toString();
57 | const previous = '?' + (new URLSearchParams({...this.state.query, page: parseInt(this.state.page) - 1})).toString();
58 | const next = '?' + (new URLSearchParams({...this.state.query, page: parseInt(this.state.page) + 1})).toString();
59 | const last = '?' + (new URLSearchParams({...this.state.query, page: this.state.totalPages})).toString();
60 | return (
61 |
64 |
65 | «
66 |
67 |
68 | ‹
69 |
70 | {
71 | this.pageNumbers()
72 | }
73 | = this.state.end}>
74 | ›
75 |
76 | = this.state.end}>
77 | »
78 |
79 |
80 | );
81 | }
82 | }
83 |
84 |
85 | export default React.forwardRef((props, ref) =>
86 | (
87 |
88 | )
89 | )
90 |
91 |
--------------------------------------------------------------------------------
/client/src/components/Common/ScrollToTop.js:
--------------------------------------------------------------------------------
1 | import { Component } from "react";
2 | import { withRouter } from "react-router";
3 |
4 | class ScrollToTop extends Component {
5 | componentDidUpdate(prevProps) {
6 | if (this.props.location !== prevProps.location) {
7 | window.scrollTo(0, 0);
8 | }
9 | }
10 |
11 | render() {
12 | return this.props.children;
13 | }
14 | }
15 |
16 | export default withRouter(ScrollToTop);
17 |
--------------------------------------------------------------------------------
/client/src/components/Common/SystemMessages.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux'
3 |
4 | class SystemMessages extends React.Component {
5 | destroyer = false;
6 |
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | messages: []
11 | }
12 | }
13 |
14 | onClick = (index) => {
15 | let messages = this.state.messages;
16 | messages.splice(index, 1);
17 | if (!messages.length) {
18 | clearTimeout(this.destroyer);
19 | this.destroyer = false;
20 | }
21 | this.setState({
22 | messages: messages
23 | });
24 | };
25 |
26 | delayedRemove = () => {
27 | if (!this.destroyer)
28 | this.destroyer = setTimeout(() => {
29 | let messages = this.state.messages;
30 | messages.pop();
31 | this.setState({
32 | messages: messages
33 | });
34 | clearTimeout(this.destroyer);
35 | this.destroyer = false;
36 | if (messages.length)
37 | this.delayedRemove();
38 | }, 4000)
39 | };
40 |
41 | componentDidUpdate = (props) => {
42 | const error = props.system && props.system.error && props.system.error.data && props.system.error.data.error;
43 | const success = props.system && props.system.message;
44 | if (error || success) {
45 | let messages = this.state.messages || [];
46 | const message = error ? {type: 'error', ...error} : {type: 'success', ...success};
47 | messages.push(message);
48 | this.setState({
49 | messages: messages
50 | });
51 | }
52 | };
53 |
54 | render() {
55 | return (
56 | {
57 | this.state.messages.map((message, key) => {
58 | const className = message.type === 'error' ? 'alert-danger' : 'alert-success';
59 | return
62 |
63 | {message.name}: {message.message}
64 | {
66 | this.onClick(key)
67 | }}>
68 | ×
69 |
70 | {this.delayedRemove(key)}
71 |
72 | })}
73 |
74 | );
75 | }
76 | }
77 |
78 | const mapStateToProps = (state) => {
79 | return {system: state.system}
80 | };
81 |
82 | export default connect(mapStateToProps, null)(SystemMessages);
83 |
--------------------------------------------------------------------------------
/client/src/components/Common/ToggleButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class ToggleButton extends React.Component {
4 |
5 | constructor(props) {
6 | super(props);
7 | this.state = {
8 | textActive: props.textActive,
9 | textPassive: props.textPassive,
10 | classActive: props.classActive || 'btn btn-default text-primary',
11 | classPassive: props.classPassive || 'btn btn-default',
12 | iconActive: props.iconActive || 'fa fa-check',
13 | iconPassive: props.iconPassive || 'fa fa-times',
14 | iconLoading: props.iconLoading || 'fa fa-circle-notch fa-spin',
15 | status: props.status,
16 | loading: props.loading
17 | }
18 | }
19 |
20 | onClick = (e) => {
21 | e.preventDefault();
22 | this.setState({
23 | loading: true
24 | });
25 | this.props.toggleFunction()
26 | // .then(response => {
27 | // console.log('toggleFunction', this.state.textActive);
28 | // // this.setState({
29 | // // status: typeof response === 'boolean' ? response : this.state.status,
30 | // // loading: false
31 | // // });
32 | // })
33 | };
34 |
35 | componentDidUpdate = (props) => {
36 | this.setState({
37 | status: props.status,
38 | loading: false
39 | });
40 | };
41 |
42 | render() {
43 | //console.log('status', this.state.status, this.props.toggleFunction);
44 | const text = this.state.status ? this.state.textActive : this.state.textPassive;
45 | const className = this.state.status ? this.state.classActive : this.state.classPassive;
46 | const icon = this.state.loading ? this.state.iconLoading
47 | : this.state.status ? this.state.iconActive : this.state.iconPassive;
48 | return (
49 |
52 | {text}
53 |
54 | );
55 | }
56 | }
57 |
58 |
59 | export default React.forwardRef((props, ref) =>
60 | (
61 |
62 | )
63 | )
64 |
65 |
--------------------------------------------------------------------------------
/client/src/components/Features/Features.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class Features extends Component {
4 | render() {
5 | return
6 |
7 |
Features
8 |
9 |
10 |
11 | Feature
12 | DONE
13 | IN PROGRESS
14 |
15 |
16 | Backend
17 |
18 |
19 |
20 |
21 |
22 | ;
23 | }
24 | }
25 |
26 | export default Features;
27 |
--------------------------------------------------------------------------------
/client/src/components/Features/Features.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/components/Features/Features.scss
--------------------------------------------------------------------------------
/client/src/components/Features/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Features';
2 |
--------------------------------------------------------------------------------
/client/src/components/Home/Home.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/components/Home/Home.css
--------------------------------------------------------------------------------
/client/src/components/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {connect} from 'react-redux';
3 | import History from "../../history";
4 |
5 |
6 | class Home extends Component {
7 | constructor(props) {
8 | super(props)
9 | this.state = {}
10 | }
11 |
12 | componentDidMount() {
13 | if (!this.props.authenticated)
14 | History.push('/signin');
15 | }
16 |
17 | static renderHello(user) {
18 | if (user) {
19 | return Hello {user} , you logged in!
20 | }
21 | }
22 |
23 | render() {
24 | return
25 |
26 | Home
27 | {this.props.me && Home.renderHello(this.props.me.name)}
28 | This is a static page and you must be logged in to see the page.
29 |
30 | ;
31 | }
32 | }
33 |
34 | const mapStateToProps = (state) => {
35 | return {authenticated: state.auth.authenticated, me: state.auth.me}
36 | };
37 |
38 | export default connect(mapStateToProps)(Home);
39 |
--------------------------------------------------------------------------------
/client/src/components/Home/Home.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Home from './Home';
4 |
5 | describe(' ', () => {
6 | test('renders', () => {
7 | const wrapper = shallow( );
8 | expect(wrapper).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/components/Home/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Home';
2 |
--------------------------------------------------------------------------------
/client/src/components/NoMatch/NoMatch.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/components/NoMatch/NoMatch.css
--------------------------------------------------------------------------------
/client/src/components/NoMatch/NoMatch.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class NoMatch extends Component {
4 | render() {
5 | return
6 |
9 | ;
10 | }
11 | }
12 |
13 | export default NoMatch;
14 |
--------------------------------------------------------------------------------
/client/src/components/NoMatch/NoMatch.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import NoMatch from './NoMatch';
4 |
5 | describe(' ', () => {
6 | test('renders', () => {
7 | const wrapper = shallow( );
8 | expect(wrapper).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/components/NoMatch/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './NoMatch';
2 |
--------------------------------------------------------------------------------
/client/src/components/Profile/Profile.css:
--------------------------------------------------------------------------------
1 | .fb-profile img.fb-image-lg {
2 | z-index: 0;
3 | width: 100%;
4 | margin-bottom: 10px;
5 | }
6 |
7 | .fb-image-profile {
8 | margin: -90px 10px 0px 50px;
9 | z-index: 9;
10 | width: 20%;
11 | }
12 |
13 | @media (max-width: 768px) {
14 |
15 | .fb-profile-text > h1 {
16 | font-weight: 700;
17 | font-size: 16px;
18 | }
19 |
20 | .fb-image-profile {
21 | margin: -45px 10px 0px 25px;
22 | z-index: 9;
23 | width: 20%;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/Profile/Profile.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {connect} from 'react-redux';
3 | import {getProfile} from "../../actions";
4 | import "./Profile.css";
5 |
6 | class Profile extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | isLoading: true,
11 | user: null
12 | }
13 | }
14 |
15 | getProfile(props) {
16 | let username = props.match.params.username || this.props.me.username;
17 | if (username !== this.state.username)
18 | this.props.getProfile(username);
19 | }
20 |
21 | componentDidUpdate(props) {
22 | if (this.props.match.params.username !== props.match.params.username) {
23 | this.setState({
24 | isLoading: true
25 | });
26 | this.getProfile(props);
27 | }
28 | if (this.state.user !== props.user) {
29 | this.setState({
30 | isLoading: false,
31 | user: props.user
32 | })
33 | }
34 | }
35 |
36 | componentDidMount() {
37 | this.getProfile(this.props)
38 | }
39 |
40 | render() {
41 | const user = this.state.user;
42 | if (this.state.isLoading) return ;
45 | if (!user.name) return
46 | User not found!
47 |
{JSON.stringify(process.env)}
48 | ;
49 | const fullName = user.name + ' ' + user.surname;
50 | return
51 |
52 |
53 |
56 |
59 |
60 |
{fullName}
61 |
{user.username}
62 |
{user.bio && user.bio}
63 |
64 |
65 |
66 |
67 |
68 | }
69 | }
70 |
71 | const mapStateToProps = (state) => {
72 | return {me: state.auth.me, user: state.system.data}
73 | };
74 |
75 | const mapDispatchToProps = (dispatch) => {
76 | return {
77 | getProfile: (values) => {
78 | dispatch(getProfile(values));
79 | },
80 | }
81 | };
82 |
83 | export default connect(mapStateToProps, mapDispatchToProps)(Profile);
84 |
--------------------------------------------------------------------------------
/client/src/components/Profile/Profile.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Profile from './Profile';
4 |
5 | describe(' ', () => {
6 | test('renders', () => {
7 | const wrapper = shallow( );
8 | expect(wrapper).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/components/Profile/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Profile';
2 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Account.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Field, Form, withFormik} from 'formik'
3 | import * as Yup from 'yup'
4 | import {settingsAccount} from '../../actions'
5 | import {bindActionCreators} from 'redux';
6 | import {connect} from 'react-redux'
7 |
8 | let setSubmittingHigher;
9 | const FormikForm = ({
10 | values,
11 | touched,
12 | errors,
13 | isSubmitting
14 | }) => (
15 |
50 | );
51 |
52 | const EnhancedForm = withFormik({
53 | mapPropsToValues({me}) {
54 | return {
55 | username: me.username || '',
56 | email: me.email || '',
57 | name: me.name || '',
58 | surname: me.surname || '',
59 | bio: me.bio || '',
60 | }
61 | },
62 | validationSchema: Yup.object().shape({
63 | username: Yup.string().required('Username is required'),
64 | email: Yup.string().email('Please write a correct email address').required('Email is required'),
65 | name: Yup.string().required('Name is required'),
66 | surname: Yup.string().required('Surname is required'),
67 | bio: Yup.string().max(200, 'Short bio must be under 200 characters or shorter')
68 | }),
69 | handleSubmit(values, {props, setSubmitting}) {
70 | setSubmittingHigher = setSubmitting;
71 | props.settingsAccount(values);
72 | }
73 | })(FormikForm);
74 |
75 | const mapStateToProps = (state) => {
76 | typeof setSubmittingHigher === 'function' && setSubmittingHigher(false);
77 | return {authenticated: state.auth.authenticated, me: state.auth.me}
78 | };
79 |
80 | const mapDispatchToProps = dispatch => (bindActionCreators({
81 | settingsAccount
82 | }, dispatch));
83 |
84 | export default connect(mapStateToProps, mapDispatchToProps)(EnhancedForm);
85 |
--------------------------------------------------------------------------------
/client/src/components/Settings/ChangePassword.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Field, Form, withFormik} from 'formik'
3 | import * as Yup from 'yup'
4 | import {settingsChangePassword} from '../../actions'
5 | import {connect} from 'react-redux'
6 |
7 | let setSubmittingHigher;
8 | const FormikForm = ({
9 | values,
10 | touched,
11 | errors,
12 | isSubmitting
13 | }) => (
14 |
15 |
40 |
41 | );
42 |
43 | const EnhancedForm = withFormik({
44 | mapPropsToValues() {
45 | return {
46 | oldPassword: '',
47 | newPassword: '',
48 | passwordConfirmation: '',
49 | }
50 | },
51 | validationSchema: Yup.object().shape({
52 | oldPassword: Yup.string().min(3, 'Password must be 3 characters or longer').required('Password is required'),
53 | newPassword: Yup.string().min(8, 'Password must be 8 characters or longer')
54 | .matches(/[a-z]/, 'Password must contain at least one lowercase char')
55 | .matches(/[A-Z]/, 'Password must contain at least one uppercase char')
56 | .matches(/[a-zA-Z]+[^a-zA-Z\s]+/, 'at least 1 number or special char (@,!,#, etc).'),
57 | passwordConfirmation: Yup.string()
58 | .oneOf([Yup.ref('newPassword'), null], 'Passwords do not match').required('Password is required')
59 | }),
60 | handleSubmit(values, {props, setSubmitting}) {
61 | setSubmittingHigher = setSubmitting;
62 | props.settingsChangePassword(values);
63 | }
64 | })(FormikForm);
65 |
66 |
67 | const mapStateToProps = (state) => {
68 | typeof setSubmittingHigher === 'function' && setSubmittingHigher(false);
69 | return {system: state.system}
70 | };
71 |
72 | const mapDispatchToProps = (dispatch) => {
73 | return {
74 | settingsChangePassword: (values) => {
75 | dispatch(settingsChangePassword(values));
76 | },
77 | }
78 | };
79 |
80 | export default connect(mapStateToProps, mapDispatchToProps)(EnhancedForm);
81 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Images.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {uploadCoverImage, uploadProfileImage} from '../../actions'
3 | import {connect} from 'react-redux'
4 |
5 | class Images extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = {
9 | me: props.me,
10 | selectedFile: '',
11 | isUploading: false,
12 | isUploadingCover: false,
13 | isUploadingProfile: false
14 | };
15 | // this.handleSubmit = this.handleSubmit.bind(this);
16 | this.handleFileChange = this.handleFileChange.bind(this);
17 | }
18 |
19 | componentDidUpdate(props) {
20 | if (this.props.me !== props.me)
21 | this.setState({
22 | me: props.me,
23 | isDone: true,
24 | isUploadingCover: false,
25 | isUploadingProfile: false,
26 | })
27 | }
28 |
29 | static handleBrowseFile(e) {
30 | e.stopPropagation();
31 | document.getElementById(e.currentTarget.dataset.input).click();
32 | }
33 |
34 | handleFileChange(e) {
35 | this.handleImageUpload(e.target.id, e.target.files[0])
36 | }
37 |
38 | async handleImageUpload(id, file) {
39 | this.setState({status: '', isUploadingCover: id === 'cover', isUploadingProfile: id === 'profile'});
40 | const formData = new FormData();
41 | formData.append('file', file, file.name);
42 | if (id === 'cover')
43 | this.props.uploadCoverImage(formData);
44 | else
45 | this.props.uploadProfileImage(formData)
46 | }
47 |
48 | render() {
49 | return
75 | }
76 | }
77 |
78 |
79 | const mapStateToProps = (state) => {
80 | return {me: state.auth.me}
81 | };
82 |
83 | const mapDispatchToProps = (dispatch) => {
84 | return {
85 | uploadCoverImage: (values) => {
86 | dispatch(uploadCoverImage(values));
87 | },
88 | uploadProfileImage: (values) => {
89 | dispatch(uploadProfileImage(values));
90 | },
91 | }
92 | };
93 |
94 | export default connect(mapStateToProps, mapDispatchToProps)(Images);
95 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Settings.js:
--------------------------------------------------------------------------------
1 | import React, {PureComponent} from 'react';
2 | import {Link, Route} from 'react-router-dom';
3 | import Account from './Account'
4 | import ChangePassword from './ChangePassword'
5 | import Images from './Images'
6 |
7 | const routes = [
8 | {
9 | path: '/settings(/account)?',
10 | link: '/settings/account',
11 | icon: 'fa fa-user',
12 | exact: true,
13 | title: () => Account ,
14 | name: 'Account',
15 | main: Account
16 | },
17 | {
18 | path: '/settings/change-password',
19 | link: '/settings/change-password',
20 | icon: 'fa fa-key',
21 | exact: true,
22 | title: () => Change Password ,
23 | name: 'Change Password',
24 | main: ChangePassword
25 | },
26 | {
27 | path: '/settings/images',
28 | link: '/settings/images',
29 | icon: 'fa fa-image',
30 | exact: true,
31 | title: () => Change Images ,
32 | name: 'Change Images',
33 | main: Images
34 | },
35 | ];
36 |
37 | class Settings extends PureComponent {
38 |
39 | render() {
40 | return
41 |
42 |
43 |
44 |
Settings
45 |
46 | {routes.map((route, index) => (
47 |
48 |
49 | {route.name}
50 |
51 | ))}
52 |
53 |
54 |
55 |
56 |
58 | {routes.map((route, index) => (
59 |
65 | ))}
66 |
67 | {routes.map((route, index) => (
68 |
74 | ))}
75 |
76 |
77 | ;
78 | }
79 | }
80 |
81 | export default Settings;
82 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Settings.scss:
--------------------------------------------------------------------------------
1 | /*.image-input {*/
2 | /*height: 0;*/
3 | /*overflow: hidden;*/
4 | /*}*/
5 |
6 | .CoverImage {
7 | position: relative;
8 | margin: 0 auto 80px;
9 | width: 100%;
10 | min-height: 200px;
11 | z-index: 98;
12 | background: #333;
13 | cursor: pointer;
14 |
15 | .loader {
16 | display: none;
17 | }
18 |
19 | .uploading {
20 | > .loader {
21 | position: absolute;
22 | width: 100%;
23 | height: 100%;
24 | display: flex;
25 | align-items: center;
26 | top: 0;
27 | background: rgba(255, 255, 255, .4);
28 | }
29 |
30 | .fa {
31 | display: flex;
32 | margin: 0 auto;
33 | color: #0F0F0F;
34 | }
35 | }
36 |
37 | img {
38 | width: 100%;
39 | height: 100%;
40 | object-fit: cover;
41 | }
42 | }
43 |
44 | .ProfileImage {
45 | position: absolute;
46 | margin-left: -80px;
47 | margin-bottom: -80px;
48 | left: 50%;
49 | bottom: 0;
50 | width: 160px;
51 | height: 160px;
52 | z-index: 99;
53 | cursor: pointer;
54 |
55 | .loader {
56 | display: none;
57 | }
58 |
59 | .uploading {
60 | > .loader {
61 | position: absolute;
62 | width: 100%;
63 | height: 100%;
64 | display: flex;
65 | align-items: center;
66 | top: 0;
67 | background: rgba(255, 255, 255, .4);
68 | }
69 |
70 | .fa {
71 | display: flex;
72 | margin: 0 auto;
73 | color: #0F0F0F;
74 | }
75 | }
76 |
77 | img {
78 | width: 100%;
79 | height: 100%;
80 | object-fit: cover;
81 | border: solid 5px #fff;
82 | border-radius: 50%;
83 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Settings.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Settings from './Settings';
4 |
5 | describe(' ', () => {
6 | test('renders', () => {
7 | const wrapper = shallow( );
8 | expect(wrapper).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/components/Settings/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Settings';
2 |
--------------------------------------------------------------------------------
/client/src/components/StaticPages/PrivacyPolicy.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class PrivacyPolicy extends Component {
4 | render() {
5 | return
6 |
7 |
Privacy Policy
8 |
9 |
This is a static page.
10 |
11 | ;
12 | }
13 | }
14 |
15 | export default PrivacyPolicy;
16 |
--------------------------------------------------------------------------------
/client/src/components/StaticPages/StaticPages.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/components/StaticPages/StaticPages.scss
--------------------------------------------------------------------------------
/client/src/components/StaticPages/TermsOfService.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class TermsOfService extends Component {
4 | render() {
5 | return
6 |
7 |
Terms of Service
8 |
9 |
This is a static page.
10 |
11 | ;
12 | }
13 | }
14 |
15 | export default TermsOfService;
16 |
--------------------------------------------------------------------------------
/client/src/components/User/Edit.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Field, Form, Formik} from 'formik'
3 | import * as Yup from 'yup'
4 | import {getUser, toggleAdmin, toggleEditor, updateUser} from '../../actions'
5 | import {connect} from 'react-redux'
6 | import History from "../../history";
7 |
8 |
9 | const FormikForm = ({
10 | values,
11 | touched,
12 | errors,
13 | status,
14 | isSubmitting
15 | }) => (
16 | );
54 |
55 | class EnhancedForm extends Component {
56 | constructor(props) {
57 | super(props)
58 | this.state = {
59 | user: {
60 | id: '',
61 | username: '',
62 | email: '',
63 | name: '',
64 | surname: '',
65 | bio: '',
66 | roles: []
67 | }
68 | }
69 | }
70 |
71 | getUserData(id) {
72 | if (!this.state.user || id !== this.state.user.id)
73 | getUser(id)
74 | .then(user => {
75 | this.setState({user: user})
76 | })
77 | .catch(error => {
78 | this.setState({'error': error})
79 | })
80 | }
81 |
82 | componentDidMount() {
83 | if (!this.props.me || !this.props.me.isAdmin)
84 | return History.goBack();
85 | this.getUserData(this.props.match.params.id)
86 | }
87 |
88 | async handleSubmit(values, {props, setFieldError, setSubmitting, setStatus}) {
89 | setStatus(null);
90 | try {
91 | await updateUser(values);
92 | setStatus({'success': 'Your account has been updated successfully!'})
93 | setSubmitting(false);
94 | } catch (errors) {
95 | setStatus({'error': errors})
96 | setSubmitting(false);
97 | }
98 | }
99 |
100 | async toggleAdmin(e) {
101 | let el = e.currentTarget;
102 | document.getElementById('errorMessage').innerHTML = '';
103 | el.disabled = true;
104 | el.getElementsByTagName('i')[0].className = 'fa fa-circle-notch fa-spin';
105 | try {
106 | let data = await toggleAdmin(el.dataset.id, el.dataset.fk);
107 | el.disabled = false;
108 | el.getElementsByTagName('i')[0].className = 'fa fa-user-secret';
109 | el.className = data && data.id ? 'btn btn-danger' : 'btn btn-secondary';
110 | } catch (error) {
111 | el.disabled = false;
112 | el.getElementsByTagName('i')[0].className = 'fa fa-user-secret';
113 | document.getElementById('errorMessage').innerHTML = '' + error + '
';
114 | }
115 | }
116 |
117 | async toggleEditor(e) {
118 | let el = e.currentTarget;
119 | document.getElementById('errorMessage').innerHTML = '';
120 | el.disabled = true;
121 | el.getElementsByTagName('i')[0].className = 'fa fa-circle-notch fa-spin';
122 | try {
123 | let data = await toggleEditor(el.dataset.id, el.dataset.fk);
124 | el.disabled = false;
125 | el.getElementsByTagName('i')[0].className = 'fa fa-user-tie';
126 | el.className = data && data.id ? 'btn btn-warning ml-1' : 'btn btn-secondary ml-1';
127 | } catch (error) {
128 | el.disabled = false;
129 | el.getElementsByTagName('i')[0].className = 'fa fa-user-tie';
130 | document.getElementById('errorMessage').innerHTML = '' + error + '
';
131 | }
132 | }
133 |
134 | render() {
135 | let adminRole = this.state.user.roles.find(x => x.name === 'admin');
136 | let editorRole = this.state.user.roles.find(x => x.name === 'editor');
137 | return (
138 |
139 |
140 |
141 |
159 |
160 |
161 |
162 |
this.toggleAdmin(e)}>
165 | Admin
166 |
167 |
this.toggleEditor(e)}>
170 | Editor
171 |
172 |
173 |
174 |
175 | )
176 | }
177 | }
178 |
179 | const mapStateToProps = (state) => {
180 | return {me: state.auth.me}
181 | }
182 |
183 | export default connect(mapStateToProps, null)(EnhancedForm);
184 |
--------------------------------------------------------------------------------
/client/src/components/User/User.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/components/User/User.css
--------------------------------------------------------------------------------
/client/src/components/User/User.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Link} from 'react-router-dom';
3 | import {connect} from 'react-redux';
4 | import {getUser} from "../../actions";
5 | import History from "../../history";
6 |
7 |
8 | class User extends Component {
9 | constructor(props) {
10 | super(props)
11 | this.state = {
12 | user: null
13 | }
14 | }
15 |
16 | getUserData(props) {
17 | let id = props.match.params.id
18 | if (!this.state.user || id !== this.state.user.id)
19 | getUser(id)
20 | .then(user => {
21 | this.setState({user: user})
22 | })
23 | .catch(error => {
24 | this.setState({'error': error})
25 | })
26 | //this.setState({ folder: params.folder || 'sketches' }, (folder) => this.loadProjects(this.state.folder));
27 | }
28 |
29 | componentDidUpdate(newProps) {
30 | this.getUserData(newProps)
31 | }
32 |
33 | componentDidMount() {
34 | if (!this.props.me || (!this.props.me.isEditor && !this.props.me.isAdmin))
35 | return History.push('/users');
36 | this.getUserData(this.props)
37 | }
38 |
39 | render() {
40 | const user = this.state.user;
41 | if (!user) return User not found!
;
42 | const fullName = user.name + ' ' + user.surname;
43 | return
44 |
45 |
46 |
{e.target.onerror = null; e.target.src="http://holder.ninja/1200x360,cover-1200x360.svg"}}
48 | src={(user.cover && user.cover.normal)}
49 | alt="{fullName}"/>
50 |
{e.target.onerror = null; e.target.src="http://holder.ninja/180x180,profile.svg"}}
52 | src={(user.image && user.image.normal)}
53 | alt="{fullName}"/>
54 |
55 |
{fullName}
56 |
{user.username}
57 |
{user.email}
58 |
{user.bio && user.bio}
59 | {this.props.me.isAdmin &&
60 |
Edit
61 | }
62 |
63 |
64 |
65 |
66 | }
67 | }
68 |
69 | const mapStateToProps = (state) => {
70 | return {me: state.auth.me}
71 | }
72 |
73 | export default connect(mapStateToProps)(User);
74 |
--------------------------------------------------------------------------------
/client/src/components/User/User.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import User from './User';
4 |
5 | describe(' ', () => {
6 | test('renders', () => {
7 | const wrapper = shallow( );
8 | expect(wrapper).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/components/User/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './User';
2 |
--------------------------------------------------------------------------------
/client/src/components/Users/Edit.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Field, Form, Formik} from 'formik'
3 | import * as Yup from 'yup'
4 | import {
5 | fetchUser,
6 | getUser,
7 | toggleAdmin,
8 | toggleEditor,
9 | toggleManager,
10 | toggleStatus,
11 | toggleWorker,
12 | updateUser
13 | } from '../../actions'
14 | import {connect} from 'react-redux'
15 | import ToggleButton from "../Common/ToggleButton";
16 |
17 | const FormikForm = ({
18 | values,
19 | touched,
20 | errors,
21 | status,
22 | isSubmitting
23 | }) => (
24 | );
62 |
63 | class EnhancedForm extends Component {
64 | constructor(props) {
65 | super(props);
66 | this.state = {
67 | user: props.user || {},
68 | // user: {
69 | // id: '',
70 | // username: '',
71 | // email: '',
72 | // name: '',
73 | // surname: '',
74 | // bio: '',
75 | // roles: []
76 | // },
77 | error: null
78 | }
79 | }
80 |
81 | getUserData(id) {
82 | if (!this.state.user || id !== this.state.user.id)
83 | getUser(id)
84 | .then(user => {
85 | this.setState({user: user})
86 | })
87 | .catch(error => {
88 | this.setState({'error': error})
89 | })
90 | }
91 |
92 | componentDidMount() {
93 | this.props.getUser(this.props.match.params.id);
94 | // if (!this.props.me || (!this.props.me.isAdmin && !this.props.me.isEditor))
95 | // return History.goBack();
96 | // this.getUserData(this.props.match.params.id)
97 | }
98 |
99 | componentDidUpdate = (props) => {
100 | if (props.user && this.props.user !== props.user)
101 | this.setState({
102 | user: props.user,
103 | loading: false
104 | });
105 | };
106 |
107 | async handleSubmit(values, {props, setFieldError, setSubmitting, setStatus}) {
108 | setStatus(null);
109 | try {
110 | await updateUser(values);
111 | setStatus({'success': 'Your account has been updated successfully!'});
112 | setSubmitting(false);
113 | } catch (errors) {
114 | setStatus({'error': errors})
115 | setSubmitting(false);
116 | }
117 | }
118 |
119 | toggleAdmin(id) {
120 | this.setState({
121 | error: null,
122 | });
123 | this.props.toggleAdmin(id)
124 | }
125 |
126 | toggleEditor(id) {
127 | this.setState({
128 | error: null,
129 | });
130 | this.props.toggleEditor(id)
131 | }
132 |
133 | toggleManager(id) {
134 | this.setState({
135 | error: null,
136 | });
137 | this.props.toggleManager(id)
138 | }
139 |
140 | toggleWorker(id) {
141 | this.setState({
142 | error: null,
143 | });
144 | this.props.toggleWorker(id)
145 | }
146 |
147 | toggleStatus(id) {
148 | this.setState({
149 | error: null,
150 | });
151 | this.props.toggleStatus(id)
152 | }
153 |
154 |
155 | render() {
156 | return (
157 |
158 |
159 |
177 |
178 |
179 | {this.state.error &&
{this.state.error}
}
180 |
181 |
182 |
183 | Admin
184 | Editor
185 | Manager
186 | Worker
187 | Status
188 |
189 |
190 |
191 | this.toggleAdmin(this.state.user.id)}/>
193 |
194 |
195 | this.toggleEditor(this.state.user.id)}/>
197 |
198 |
199 | this.toggleManager(this.state.user.id)}/>
201 |
202 |
203 | this.toggleWorker(this.state.user.id)}/>
205 |
206 |
207 | this.toggleStatus(this.state.user.id)}/>
209 |
210 |
211 |
212 |
213 |
214 |
215 |
)
216 | }
217 | }
218 |
219 | const mapStateToProps = (state) => {
220 | return {me: state.auth.me, user: state.system.data}
221 | };
222 |
223 | const mapDispatchToProps = (dispatch) => {
224 | return {
225 | getUser: (values) => {
226 | dispatch(fetchUser(values));
227 | },
228 | toggleAdmin: (values) => {
229 | dispatch(toggleAdmin(values));
230 | },
231 | toggleEditor: (values) => {
232 | dispatch(toggleEditor(values));
233 | },
234 | toggleManager: (values) => {
235 | dispatch(toggleManager(values));
236 | },
237 | toggleWorker: (values) => {
238 | dispatch(toggleWorker(values));
239 | },
240 | toggleStatus: (values) => {
241 | dispatch(toggleStatus(values));
242 | },
243 | }
244 | };
245 |
246 | export default connect(mapStateToProps, mapDispatchToProps)(EnhancedForm);
247 |
--------------------------------------------------------------------------------
/client/src/components/Users/List.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {Link} from 'react-router-dom';
3 | import {connect} from 'react-redux';
4 | import {fetchUsers} from '../../actions';
5 | import Pagination from "../Common/Pagination";
6 | import History from "../../history";
7 | import {Input} from "reactstrap";
8 |
9 | class List extends Component {
10 |
11 | constructor(props) {
12 | super(props);
13 | const params = new URLSearchParams(props.location.search);
14 | const search = params.get('search') || '';
15 | const page = params.get('page') || 1;
16 | this.state = {
17 | me: props.me,
18 | count: 0,
19 | page,
20 | limit: 10,
21 | range: 10,
22 | items: [],
23 | query: search,
24 | search: search,
25 | filteredItems: false,
26 | }
27 | }
28 |
29 | componentDidMount() {
30 | if (!this.state.me)
31 | return History.push('/signin');
32 | console.log('componentDidMount', this.state);
33 | if (this.state.query) {
34 | const filter = {skip: Math.max(0, this.state.page - 1) * this.state.limit, limit: this.state.limit};
35 | this.props.fetchList({query: this.state.query, filter});
36 | } else {
37 | this.props.fetchList();
38 | }
39 | }
40 |
41 | componentDidUpdate(props) {
42 | const params = new URLSearchParams(props.location.search);
43 | const search = params.get('search') || '';
44 | const page = params.get('page') || 1;
45 | console.log('componentDidUpdate', props);
46 |
47 | if (page !== this.state.page) {
48 | console.log('page', props);
49 | const filter = {skip: Math.max(0, page - 1) * this.state.limit, limit: this.state.limit};
50 | this.setState({page: page});
51 | this.props.fetchList({query: this.state.query, filter});
52 | } else if (search !== this.state.search) {
53 | console.log('search', props);
54 | this.setState({search});
55 | this.props.fetchList({query: search});
56 | }
57 |
58 | if (props.data && props.data.items) {
59 | this.setState({
60 | isLoading: false,
61 | count: props.data.count,
62 | items: props.data.items,
63 | filteredItems: false
64 | });
65 | }
66 | }
67 |
68 | handleFilter = (e) => {
69 | e.preventDefault();
70 | console.log(e.currentTarget.value);
71 | const query = e.currentTarget.value.toLowerCase();
72 | const filteredItems = this.state.items.filter(item => {
73 | console.log({item});
74 | return (item.name.toLowerCase().indexOf(query) > -1 || item.surname.toLowerCase().indexOf(query) > -1
75 | || item.username.toLowerCase().indexOf(query) > -1 || item.email.toLowerCase().indexOf(query) > -1)
76 | });
77 | this.setState({query, filteredItems});
78 | };
79 |
80 | handleSearch = (e) => {
81 | e.preventDefault();
82 | console.log(e.key);
83 | if (e.key === 'Enter')
84 | return History.push('?search=' + this.state.query);
85 | };
86 |
87 | renderItems() {
88 | return (this.state.filteredItems || this.state.items).map(user => {
89 | return
90 |
91 |
92 |
96 |
97 |
98 | {user.username}
99 |
100 | {user.name} {user.surname}
101 |
102 |
103 | PROFILE
104 | {(this.props.me.isAdmin || this.props.me.isEditor) &&
105 | EDIT}
106 |
107 |
108 |
109 |
110 |
;
111 | })
112 | }
113 |
114 | render() {
115 | if (!this.state.items) {
116 | return Loading...
;
117 | }
118 | return (
119 |
120 |
121 |
123 | Total result: {this.state.count}
124 |
125 | {this.renderItems()}
126 |
127 | {this.state.count &&
128 |
}
131 |
132 |
133 | );
134 | }
135 | }
136 |
137 | const mapStateToProps = (state) => {
138 | return {me: state.auth.me, data: state.system.data}
139 | };
140 |
141 | const mapDispatchToProps = (dispatch) => {
142 | return {
143 | fetchList: (params) => {
144 | dispatch(fetchUsers(params));
145 | },
146 | }
147 | };
148 |
149 | export default connect(mapStateToProps, mapDispatchToProps)(List);
150 |
--------------------------------------------------------------------------------
/client/src/components/Users/Users.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Link, Route} from 'react-router-dom';
3 | import List from "./List";
4 | import Edit from "./Edit";
5 |
6 | const routes = [
7 | {
8 | path: '/users(/list)?',
9 | link: '/users/list',
10 | icon: 'fa fa-users',
11 | exact: true,
12 | title: () => 'User List',
13 | name: 'List',
14 | main: List
15 | },
16 | {
17 | path: '/users/edit/:id?',
18 | link: null,
19 | icon: 'fa fa-image',
20 | exact: true,
21 | title: () => 'Edit User',
22 | name: 'Edit',
23 | main: Edit
24 | },
25 | ];
26 |
27 | class Users extends React.PureComponent {
28 |
29 | render() {
30 | return
31 |
32 |
33 |
34 |
User Management
35 |
36 | {routes.map((route, index) => (
37 | route.link &&
38 |
39 | {route.name}
40 |
41 | ))}
42 |
43 |
44 |
45 |
46 |
47 | {routes.map((route, index) => (
48 |
54 | ))}
55 |
56 | {routes.map((route, index) => (
57 |
63 | ))}
64 |
65 |
66 | ;
67 | }
68 | }
69 |
70 | export default Users;
--------------------------------------------------------------------------------
/client/src/components/Users/Users.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/components/Users/Users.scss
--------------------------------------------------------------------------------
/client/src/components/Users/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Users';
2 |
--------------------------------------------------------------------------------
/client/src/components/Welcome/Welcome.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | class Welcome extends Component {
4 | render() {
5 | return
6 |
17 |
18 |
19 |
20 |
21 |
22 |
ABOUT
23 |
This is a default static page and no need
24 | authentication.
25 |
Hi, I'm Goker and I'm developing the project for experience a full-stack React application
26 | with LoopBack - Node.js framework. If you want to contribute you can reach me freely via goker.me
30 |
31 |
32 |
33 | ;
34 | }
35 | }
36 |
37 | export default Welcome;
38 |
--------------------------------------------------------------------------------
/client/src/components/Welcome/Welcome.scss:
--------------------------------------------------------------------------------
1 | header.intro {
2 | text-align: center;
3 | color: white;
4 | background-image: url('../../styles/img/intro-header-bg.jpg');
5 | background-repeat: no-repeat;
6 | background-attachment: scroll;
7 | background-position: center center;
8 | background-size: cover;
9 | .intro-text {
10 | padding-top: 150px;
11 | padding-bottom: 100px;
12 | .intro-lead-in {
13 | font-size: 22px;
14 | font-style: italic;
15 | line-height: 22px;
16 | margin-bottom: 25px;
17 | @include serif-font;
18 | }
19 | .intro-heading {
20 | font-size: 40px;
21 | font-weight: 700;
22 | line-height: 50px;
23 | margin-bottom: 25px;
24 | @include heading-font;
25 | .highlight {
26 | display: inline-block;
27 | padding: 0 10px;
28 | background: $black;
29 | border-radius: 2px;
30 | }
31 | }
32 | }
33 | }
34 | @media(min-width:768px) {
35 | header.intro {
36 | .intro-text {
37 | padding-top: 200px;
38 | padding-bottom: 200px;
39 | .intro-lead-in {
40 | font-size: 40px;
41 | font-style: italic;
42 | line-height: 40px;
43 | margin-bottom: 25px;
44 | @include serif-font;
45 | }
46 | .intro-heading {
47 | font-size: 75px;
48 | font-weight: 700;
49 | line-height: 75px;
50 | margin-bottom: 50px;
51 | @include heading-font;
52 | }
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/client/src/components/Welcome/Welcome.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import Welcome from './Welcome';
4 |
5 | describe(' ', () => {
6 | test('renders', () => {
7 | const wrapper = shallow( );
8 | expect(wrapper).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/client/src/components/Welcome/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Welcome';
2 |
--------------------------------------------------------------------------------
/client/src/history.js:
--------------------------------------------------------------------------------
1 |
2 | import { createBrowserHistory } from 'history';
3 |
4 | export default createBrowserHistory();
5 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import {Provider} from 'react-redux';
4 | import {applyMiddleware, compose, createStore} from 'redux';
5 | import reduxThunk from 'redux-thunk';
6 | import {BrowserRouter, Router} from 'react-router-dom';
7 |
8 | import ScrollToTop from './components/Common/ScrollToTop';
9 | import History from './history';
10 | import Routes from './routes';
11 |
12 | import rootReducer from './reducers';
13 | import {getSession} from './actions';
14 | import * as type from "./actions/types";
15 |
16 | import * as serviceWorker from './serviceWorker';
17 |
18 | /*
19 | // UNCOMMENT IT FOR PRODUCTION
20 | const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
21 | const store = createStoreWithMiddleware(rootReducer);
22 | */
23 |
24 | /* COMMENT IT OUT FOR PRODUCTION */
25 | const store = createStore(
26 | rootReducer,
27 | compose(
28 | applyMiddleware(reduxThunk),
29 | window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f
30 | )
31 | );
32 |
33 | getSession((me) => {
34 | me && store.dispatch({type: type.AUTH_USER, payload: me});
35 | ReactDOM.render(
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | , document.getElementById('root'));
45 | // If you want your app to work offline and load faster, you can change
46 | // unregister() to register() below. Note this comes with some pitfalls.
47 | // Learn more about service workers: http://bit.ly/CRA-PWA
48 | serviceWorker.unregister();
49 | });
50 |
51 |
--------------------------------------------------------------------------------
/client/src/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import {AUTH_ERROR, AUTH_USER, UNAUTH_USER} from '../actions/types';
2 |
3 | export const reducer = (state = {}, action) => {
4 |
5 | switch (action.type) {
6 | case AUTH_USER:
7 | return {...state, error: '', authenticated: true, me: action.payload};
8 | case UNAUTH_USER:
9 | return {...state, authenticated: false, me: false};
10 | case AUTH_ERROR:
11 | return {...state, error: action.payload};
12 | default:
13 | return state;
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux';
2 | import {reducer as systemReducer} from './system';
3 | import {reducer as authReducer} from './auth';
4 | import {reducer as settingsReducer} from './settings';
5 |
6 | const rootReducer = combineReducers({
7 | system: systemReducer,
8 | auth: authReducer,
9 | settings: settingsReducer
10 | });
11 |
12 | export default rootReducer;
13 |
--------------------------------------------------------------------------------
/client/src/reducers/settings.js:
--------------------------------------------------------------------------------
1 | import {CHANGEPASSWORD_ERROR, CHANGEPASSWORD_SUCCESS} from '../actions/types';
2 |
3 | export const reducer = (state = {}, action) => {
4 |
5 | switch (action.type) {
6 | case CHANGEPASSWORD_ERROR:
7 | return {...state, error: action.payload}
8 | case CHANGEPASSWORD_SUCCESS:
9 | return {...state, success: action.payload}
10 | default:
11 | return state;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/client/src/reducers/system.js:
--------------------------------------------------------------------------------
1 | import {DATA, ERROR, FILE, SUCCESS} from '../actions/types';
2 |
3 | export const reducer = (state = {}, action) => {
4 | switch (action.type) {
5 | case SUCCESS:
6 | return {...state, error: '', message: action.payload};
7 | case DATA:
8 | return {...state, error: '', message: '', data: action.payload};
9 | case FILE:
10 | return {...state, error: '', message: '', file: action.payload};
11 | case ERROR:
12 | return {...state, error: action.payload, message: ''};
13 | default:
14 | return state;
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/routes/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Route, Switch} from 'react-router-dom';
3 | import App from '../components/App';
4 | import SignUp from '../components/Auth/SignUp';
5 | import SignIn from '../components/Auth/SignIn';
6 | import SignOut from '../components/Auth/SignOut';
7 | import Reset from '../components/Auth/Reset';
8 | import NewPassword from '../components/Auth/NewPassword';
9 | import Welcome from '../components/Welcome';
10 | import Features from '../components/Features';
11 | import Home from '../components/Home';
12 | import Profile from '../components/Profile';
13 | import Settings from '../components/Settings';
14 | import Users from '../components/Users';
15 | import User from '../components/User';
16 | import UserEdit from '../components/User/Edit';
17 | import NoMatch from '../components/NoMatch';
18 | import TermsOfService from "../components/StaticPages/TermsOfService";
19 | import PrivacyPolicy from "../components/StaticPages/PrivacyPolicy";
20 |
21 | const Routes = () => {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default Routes;
50 |
--------------------------------------------------------------------------------
/client/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/client/src/styles/App.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 | @import 'mixins';
3 | @import 'keyframes';
4 |
5 | @import "~bootstrap/scss/bootstrap";
6 | @import '~nprogress/nprogress';
7 |
8 | @import 'global';
9 | @import 'navbar';
10 | @import 'sidebar';
11 | @import 'main';
12 | @import 'forms';
13 |
14 |
15 | @import '../components/Welcome/Welcome';
16 | @import '../components/Features/Features';
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/src/styles/_forms.scss:
--------------------------------------------------------------------------------
1 | .image-input {
2 | position: relative;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | overflow: hidden;
7 |
8 | .input-container {
9 | height: 0;
10 | overflow: hidden;
11 | }
12 |
13 | img {
14 | height: 100%;
15 | object-fit: contain;
16 | cursor: pointer;
17 | }
18 |
19 | i {
20 | cursor: pointer;
21 | }
22 | }
23 |
24 | .image-input-container {
25 | height: 0;
26 | overflow: hidden;
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/styles/_global.scss:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | }
4 |
5 | #root, .page {
6 | display: flex;
7 | flex-direction: column;
8 | flex-grow: 1;
9 | width: 100%;
10 | min-height: 100%;
11 | }
12 |
13 | section {
14 | padding: 120px 0;
15 |
16 | &.cover {
17 | display: flex;
18 | flex-grow: 1;
19 | height: 100%;
20 | }
21 | }
22 |
23 | .lined {
24 | display: flex;
25 | width: 100%;
26 | justify-content: center;
27 | align-items: center;
28 | text-align: center;
29 |
30 | &:before,
31 | &:after {
32 | content: '';
33 | border-top: 2px solid $gray-300;
34 | margin: 0 20px 0 0;
35 | flex: 1 0 20px;
36 | }
37 |
38 | &:after {
39 | margin: 0 0 0 20px;
40 | }
41 |
42 | &.lined-primary {
43 | &:before,
44 | &:after {
45 | border-color: $primary;
46 | }
47 | }
48 |
49 | &.lined-secondary {
50 | &:before,
51 | &:after {
52 | border-color: $secondary;
53 | }
54 | }
55 |
56 | &.lined-third {
57 | &:before,
58 | &:after {
59 | border-color: $third;
60 | }
61 | }
62 |
63 | &.lined-fourth {
64 | &:before,
65 | &:after {
66 | border-color: $fourth;
67 | }
68 | }
69 |
70 | &.lined-align-left {
71 | text-align: left;
72 |
73 | &:before {
74 | border: 0;
75 | margin: 0;
76 | flex: 0;
77 | }
78 | }
79 |
80 | &.lined-align-right {
81 | text-align: right;
82 |
83 | &:after {
84 | border: 0;
85 | margin: 0;
86 | flex: 0;
87 | }
88 | }
89 | }
90 |
91 | .system-messages {
92 | animation: shake 0.82s cubic-bezier(.36, .07, .19, .97) both;
93 | animation-iteration-count: 2;
94 | }
95 |
96 | .page-item {
97 | &.disabled {
98 | a {
99 | color: $gray-500;
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/client/src/styles/_intro.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/styles/_intro.scss
--------------------------------------------------------------------------------
/client/src/styles/_keyframes.scss:
--------------------------------------------------------------------------------
1 | @keyframes shake {
2 | 10%, 90% {
3 | transform: translate3d(-1px, 0, 0);
4 | }
5 |
6 | 20%, 80% {
7 | transform: translate3d(2px, 0, 0);
8 | }
9 |
10 | 30%, 50%, 70% {
11 | transform: translate3d(-4px, 0, 0);
12 | }
13 |
14 | 40%, 60% {
15 | transform: translate3d(4px, 0, 0);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/styles/_main.scss:
--------------------------------------------------------------------------------
1 | .main-heading {
2 | font-size: .75rem;
3 | text-transform: uppercase;
4 | }
--------------------------------------------------------------------------------
/client/src/styles/_mixins.scss:
--------------------------------------------------------------------------------
1 | // Font Mixins
2 | @mixin serif-font {
3 | font-family: 'Droid Serif', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
4 | }
5 | @mixin script-font {
6 | font-family: 'Kaushan Script', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
7 | }
8 | @mixin body-font {
9 | font-family: 'Roboto Slab', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
10 | }
11 | @mixin heading-font {
12 | font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/styles/_navbar.scss:
--------------------------------------------------------------------------------
1 | #navbar {
2 | background-color: $primary;
3 | border-bottom: 1px dashed $gray-900;
4 | .navbar-toggler {
5 | font-size: 12px;
6 | right: 0;
7 | padding: 13px;
8 | text-transform: uppercase;
9 | color: white;
10 | background-color: $primary;
11 | @include heading-font;
12 | }
13 | .navbar-brand {
14 | img {
15 | height: 42px;
16 | }
17 | &.active,
18 | &:active,
19 | &:focus,
20 | &:hover {
21 | color: darken($black, 10%);
22 | }
23 | }
24 | .navbar-avatar {
25 | margin: -10px 10px -10px 0;
26 | width: 40px;
27 | height: 40px;
28 | object-fit: cover;
29 | border: solid 2px $white;
30 | border-radius: 50%;
31 | box-shadow: 0 0 2px $gray-200;
32 | }
33 | .navbar-nav {
34 | .nav-item {
35 | .nav-link {
36 | font-size: 90%;
37 | font-weight: 400;
38 | padding: 0.75em 0;
39 | letter-spacing: 1px;
40 | color: $gray-900;
41 | @include heading-font;
42 | &.active,
43 | &:hover {
44 | color: $black;
45 | }
46 | }
47 | }
48 | }
49 | .dropdown-menu {
50 | margin-top: 0;
51 | border: 0;
52 | border-radius: 0 0 4px 4px;
53 | box-shadow: 0 .5em .5em $gray-300;
54 | }
55 | }
56 |
57 | @media(min-width: 992px) {
58 | #navbar {
59 | padding-top: 25px;
60 | padding-bottom: 25px;
61 | transition: padding-top 0.3s, padding-bottom 0.3s;
62 | background-color: transparent;
63 | .navbar-brand img {
64 | transition: all 0.3s;
65 | }
66 | .navbar-nav {
67 | .nav-item {
68 | .nav-link {
69 | padding: 1.1em 1em !important;
70 | }
71 | }
72 | }
73 | &.navbar-shrink {
74 | padding-top: 0;
75 | padding-bottom: 0;
76 | background-color: $white;
77 | border-bottom: 0;
78 | box-shadow: 0 0 .5em $gray-300;
79 | .navbar-brand {
80 | img {
81 | height: 32px;
82 | }
83 | }
84 | }
85 | }
86 | #systemMessages {
87 | top: 60px;
88 | position: absolute;
89 | display: flex;
90 | width: 100%;
91 | flex-direction: column-reverse;
92 | justify-content: center;
93 | padding: 1em;
94 | }
95 | }
96 |
97 |
--------------------------------------------------------------------------------
/client/src/styles/_profile.scss:
--------------------------------------------------------------------------------
1 | .profile {
2 | @include serif-font;
3 | .coverImage {
4 |
5 | }
6 | }
7 | @media(min-width:768px) {
8 |
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/styles/_sidebar.scss:
--------------------------------------------------------------------------------
1 | .sidebar-heading {
2 | font-size: .75rem;
3 | text-transform: uppercase;
4 | }
--------------------------------------------------------------------------------
/client/src/styles/_variables.scss:
--------------------------------------------------------------------------------
1 | $primary: #00bdc8 !default;
2 | $secondary: #5d2749 !default;
3 | $third: #fc4f5c !default;
4 | $fourth: #ffd37f !default;
5 |
--------------------------------------------------------------------------------
/client/src/styles/img/intro-header-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gokerDEV/loopback-react/dd91ed094e6659f00eafbf221cd23d26a278b7c8/client/src/styles/img/intro-header-bg.jpg
--------------------------------------------------------------------------------
/client/src/styles/img/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
13 |
17 |
20 |
24 |
28 |
29 |
31 |
32 |
33 |
34 |
37 |
43 |
44 |
47 |
48 |
49 |
50 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/common/mixins/FullName.js:
--------------------------------------------------------------------------------
1 | module.exports = function (Model, options) {
2 | 'use strict';
3 | Model.observe('before save', function event(ctx, next) {
4 | if (ctx.instance) {
5 | ctx.instance.unsetAttribute('fullname');
6 | } else {
7 | delete ctx.data.fullname;
8 | }
9 | next();
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/common/mixins/Tags.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function (Model, options) {
3 | // give each dog a unique tag for tracking
4 | Model.defineProperty('tags', {
5 | type: String,
6 | 'defaultFn': 'uuid',
7 | index: true,
8 | unique: true,
9 | });
10 | };
11 |
--------------------------------------------------------------------------------
/common/mixins/TimeStamp.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function (Model, options) {
3 | Model.defineProperty('createdAt', {type: Date, default: '$now'});
4 | Model.defineProperty('updatedAt', {type: Date, default: '$now'});
5 | Model.observe('before save', function event(ctx, next) { //Observe any insert/update event on Model
6 | if (ctx.instance) {
7 | ctx.instance.updatedAt = new Date();
8 | } else {
9 | ctx.data.updatedAt = new Date();
10 | }
11 | next();
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/common/models/file.js:
--------------------------------------------------------------------------------
1 | var CONTAINERS_URL = '/api/containers/';
2 | module.exports = function (File) {
3 |
4 | File.upload = function (ctx, options, cb) {
5 | if (!options) options = {};
6 | ctx.req.params.container = 'common';
7 | File.app.models.container.upload(ctx.req, ctx.result, options, function (err, fileObj) {
8 | if (err) {
9 | cb(err);
10 | } else {
11 | var fileInfo = fileObj.files.file[0];
12 | File.create({
13 | name: fileInfo.name,
14 | type: fileInfo.type,
15 | container: fileInfo.container,
16 | url: CONTAINERS_URL + fileInfo.container + '/download/' + fileInfo.name
17 | }, function (err, obj) {
18 | if (err !== null) {
19 | cb(err);
20 | } else {
21 | cb(null, obj);
22 | }
23 | });
24 | }
25 | });
26 | };
27 |
28 | File.remoteMethod(
29 | 'upload',
30 | {
31 | description: 'Uploads a file',
32 | accepts: [
33 | {arg: 'ctx', type: 'object', http: {source: 'context'}},
34 | {arg: 'options', type: 'object', http: {source: 'query'}}
35 | ],
36 | returns: {
37 | arg: 'fileObject', type: 'object', root: true
38 | },
39 | http: {verb: 'post'}
40 | }
41 | );
42 |
43 | };
44 |
--------------------------------------------------------------------------------
/common/models/file.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "file",
3 | "base": "PersistedModel",
4 | "idInjection": true,
5 | "options": {
6 | "validateUpsert": true
7 | },
8 | "properties": {
9 | "name": {
10 | "type": "string"
11 | },
12 | "type": {
13 | "type": "string"
14 | },
15 | "url": {
16 | "type": "string",
17 | "required": true
18 | }
19 | },
20 | "validations": [],
21 | "relations": {},
22 | "acls": [],
23 | "methods": {}
24 | }
25 |
--------------------------------------------------------------------------------
/common/models/user.js:
--------------------------------------------------------------------------------
1 | const config = require('../../server/config.json');
2 | const sharp = require('sharp');
3 | const nodemailer = require("nodemailer");
4 | const mandrillTransport = require('nodemailer-mandrill-transport');
5 | const from = config.fromMail;
6 |
7 | const transport = nodemailer.createTransport(mandrillTransport({
8 | auth: {
9 | apiKey: config.mandrillApiKey
10 | },
11 | mandrillOptions: {
12 | async: false
13 | }
14 | }));
15 |
16 | const templateMail = function (title, html) {
17 | return `
18 |
19 |
20 | ${title}
21 |
22 |
23 |
25 | ${html}
26 | You’re receiving this email because you have an account in
27 | ${config.name} . If you are not sure why you’re receiving this, please contact us.
28 |
29 | `;
30 | };
31 |
32 | module.exports = function (User) {
33 | User.observe('before save', function (context, next) {
34 | if (context.instance)
35 | delete context.instance.unsetAttribute('roles');
36 | else
37 | delete context.data.roles;
38 | next();
39 | });
40 | User.afterRemote('create', function (context, user, next) {
41 | User.generateVerificationToken(user, null, function (err, token) {
42 | if (err) {
43 | return next(err);
44 | }
45 | console.log('USER', user);
46 | user.status = true;
47 | user.verificationToken = token;
48 | user.save(function (err) {
49 | if (err) {
50 | return next(err);
51 | }
52 | const title = 'Account Verification';
53 | const link = config.url + '/api/users/confirm?uid='
54 | + user.id + '&redirect=/signin&token=' + token;
55 | const html = `Hi ${user.name} ;
56 | Thanks so much joining ${config.name}! To finish your register you just need to confirm that we got your email right.
57 |
58 |
59 | Confirm your account
60 | Button not working? Try pasting this link into your browser:
61 | ${link}
`;
62 |
63 | // User.app.models.Email.send({
64 | // to: user.email,
65 | // from: from,
66 | // subject: title,
67 | // html: templateMail(title, html)
68 | // }, function (err, res) {
69 | // if (err) return console.log('> error sending verify email', err);
70 | // console.log('> sending verify reset email to:', user.email, res);
71 | // });
72 | console.log('VERIFICATION LINK', link);
73 | transport.sendMail({
74 | mandrillOptions: {
75 | async: false
76 | },
77 | from: from,
78 | to: user.email,
79 | subject: title,
80 | html: templateMail(title, html)
81 | }, function (err, info) {
82 | if (err) {
83 | console.error(err);
84 | } else {
85 | console.log(info);
86 | }
87 | });
88 | });
89 | });
90 | next();
91 | });
92 | // // Method to render
93 | // User.afterRemote('prototype.verify', function (context, user, next) {
94 | // context.res.render('response', {
95 | // title: 'A Link to verify your identity has been sent ' +
96 | // 'to your email successfully',
97 | // content: 'Please check your email and click on the verification link ' +
98 | // 'before logging in',
99 | // redirectTo: '/',
100 | // redirectToLinkText: 'Log in'
101 | // });
102 | // });
103 | //send password reset link when requested
104 | User.on('resetPasswordRequest', function (user) {
105 | const url = config.url + '/newpassword';
106 | const html = `Click the link to reset your password
107 | ${url}/${user.accessToken.id} `;
108 | console.log('html', html);
109 | transport.sendMail({
110 | from: from,
111 | to: user.email,
112 | subject: 'Password Reset',
113 | html: templateMail('Password Reset', html)
114 | }, function (err, info) {
115 | if (err) {
116 | return console.log('Error sending password reset email');
117 | } else {
118 | console.log('Sending password reset email to:', user.email, info);
119 | }
120 | });
121 | });
122 | User.approve = function (id, cb) {
123 | User.findById(id, function (err, user) {
124 | if (err) {
125 | cb(err);
126 | } else {
127 | user.emailVerified = true;
128 | user.adminVerified = true;
129 | user.save(function (err) {
130 | if (err) {
131 | console.log(err);
132 | next(err);
133 | } else {
134 | const html = 'Good News ' +
135 | '
We have just approved your account. ' +
136 | 'You can play ' + config.name + ' the game whenever you want.
';
137 | transport.sendMail({
138 | from: from,
139 | to: user.email,
140 | subject: 'Account Approved',
141 | html: templateMail('Account Approved', html)
142 | }, function (err, info) {
143 | if (err) {
144 | console.log(err);
145 | cb(err);
146 | }
147 | console.log('Sending password reset email to:', user.email, info);
148 | cb(null, user);
149 | });
150 | }
151 | });
152 | }
153 | });
154 | };
155 | User.profile = function (username, cb) {
156 | User.findOne({"where": {"username": username}}, function (err, user) {
157 | if (err) {
158 | cb(err);
159 | } else {
160 | cb(null, user);
161 | }
162 | });
163 | };
164 | User.cover = function (id, context, options, cb) {
165 | const Container = User.app.models.Container;
166 | const token = options && options.accessToken;
167 | const root = User.app.dataSources.storage.settings.root;
168 | User.findById(token && token.userId, function (err, user) {
169 | if (err) {
170 | cb(err);
171 | } else {
172 | // each user has own container as named by user id
173 | Container.putContainer(user.id.toString(), function (err, container) {
174 | if (err) {
175 | cb(err);
176 | } else {
177 | console.log('CONTAINER CHECK', container);
178 | Container.upload(context.req, context.res, {container: container}, function (err, file) {
179 | if (err) {
180 | cb(err);
181 | } else {
182 | file = file.files.file && file.files.file.pop();
183 | console.log('FILE UPLOADING', file);
184 | //console.log('USER IMAGE', user);
185 |
186 | const normal = file.name.replace(/\./, '_normal.');
187 | sharp(root + file.container + '/' + file.name)
188 | .resize(1200, 360, {fit: 'cover'})
189 | .on('error', function (err) {
190 | console.log(err);
191 | })
192 | .jpeg()
193 | .toFile(root + file.container + '/' + normal)
194 | .then(function () {
195 | const thumb = file.name.replace(/\./, '_thumb.');
196 | sharp(root + file.container + '/' + file.name)
197 | .resize(400, 120)
198 | .toFile(root + file.container + '/' + thumb)
199 | .then(function () {
200 | user.updateAttributes({
201 | 'cover': {
202 | name: file.name,
203 | type: file.type,
204 | container: file.container,
205 | url: '/api/containers/' + file.container + '/download/' + file.name,
206 | normal: '/api/containers/' + file.container + '/download/' + normal,
207 | thumb: '/api/containers/' + file.container + '/download/' + thumb
208 | }
209 | }, function (err) {
210 | if (err) {
211 | console.log(err);
212 | cb(err);
213 | }
214 | console.log('USER COVER SAVED', user);
215 | cb(null, user);
216 | //return user.image;
217 | });
218 |
219 | })
220 | .catch(err => {
221 | throw err
222 | });
223 |
224 | })
225 | .catch(err => {
226 | throw err
227 | });
228 | }
229 | });
230 | }
231 | });
232 | }
233 | });
234 | };
235 | User.image = function (id, context, options, cb) {
236 | const Container = User.app.models.Container;
237 | const token = options && options.accessToken;
238 | const root = User.app.dataSources.storage.settings.root;
239 | //console.log(cb);
240 | //cb = function(){return console.log()};
241 | User.findById(token && token.userId, function (err, user) {
242 | if (err) {
243 | cb(err);
244 | } else {
245 | // each user has own container as named by user id
246 | Container.putContainer(user.id.toString(), function (err, container) {
247 | if (err) {
248 | cb(err);
249 | } else {
250 | Container.upload(context.req, context.res, {container: container}, function (err, file) {
251 | if (err) {
252 | cb(err);
253 | } else {
254 | file = file.files.file.pop();
255 | //console.log('FILE UPLOADED', file);
256 |
257 | console.log('FILE UPLOADED', root + file.container + '/' + file.name);
258 |
259 | const normal = file.name.replace(/\./, '_normal.');
260 |
261 | sharp(root + file.container + '/' + file.name)
262 | .resize(360, 360, {fit: 'cover'})
263 | //.crop(sharp.strategy.attention)
264 | .on('error', function (err) {
265 | console.log(err);
266 | })
267 | .jpeg()
268 | .toFile(root + file.container + '/' + normal)
269 | .then(function () {
270 | const thumb = file.name.replace(/\./, '_thumb.');
271 | sharp(root + file.container + '/' + file.name)
272 | .resize(50, 50)
273 | .toFile(root + file.container + '/' + thumb)
274 | .then(function () {
275 | user.updateAttributes({
276 | 'image': {
277 | name: file.name,
278 | type: file.type,
279 | container: file.container,
280 | url: '/api/containers/' + file.container + '/download/' + file.name,
281 | normal: '/api/containers/' + file.container + '/download/' + normal,
282 | thumb: '/api/containers/' + file.container + '/download/' + thumb
283 | }
284 | }, function (err) {
285 | if (err) {
286 | console.log(err);
287 | cb(err);
288 | }
289 | console.log('USER IMAGE SAVED', user);
290 | cb(null, user);
291 | //return user.image;
292 | })
293 |
294 | })
295 | .catch(err => {
296 | throw err
297 | });
298 |
299 | })
300 | .catch(err => {
301 | throw err
302 | });
303 |
304 |
305 | }
306 | });
307 | }
308 | });
309 | }
310 | });
311 | };
312 |
313 | const setACL = (id, cb, type) => {
314 | const Role = User.app.models.Role;
315 | const RoleMapping = User.app.models.RoleMapping;
316 | User.findById(id, function (err, user) {
317 | if (err) {
318 | cb(err);
319 | } else {
320 | Role.findOrCreate({where: {name: type}}, {
321 | name: type
322 | }, function (err, role) {
323 | if (err) cb(err);
324 | RoleMapping.find({
325 | where: {
326 | principalId: user.id,
327 | roleId: role.id
328 | }
329 | }, function (err, principal) {
330 | if (err) cb(err);
331 | if (!principal.length)
332 | RoleMapping.create({
333 | principalType: RoleMapping.USER,
334 | principalId: user.id,
335 | roleId: role.id
336 | }, function (err) {
337 | if (err) cb(err);
338 | User.findById(id, function (err, user) {
339 | if (err) {
340 | cb(err);
341 | }
342 | cb(null, user);
343 | });
344 | });
345 | else
346 | RoleMapping.destroyById(principal[0].id, function (err) {
347 | if (err) cb(err);
348 | User.findById(id, function (err, user) {
349 | if (err) {
350 | cb(err);
351 | }
352 | cb(null, user);
353 | });
354 | });
355 | });
356 | });
357 | }
358 | });
359 | };
360 | User.toggleAdmin = function (id, cb) {
361 | setACL(id, cb, 'admin');
362 | };
363 | User.toggleEditor = function (id, cb) {
364 | setACL(id, cb, 'editor');
365 | };
366 | User.toggleManager = function (id, cb) {
367 | setACL(id, cb, 'manager');
368 | };
369 | User.toggleWorker = function (id, cb) {
370 | setACL(id, cb, 'worker');
371 | };
372 | User.toggleStatus = function (id, cb) {
373 | User.findById(id, function (err, user) {
374 | if (err) {
375 | cb(err);
376 | } else {
377 | user.status = !user.status;
378 | user.save(function (err) {
379 | if (err) {
380 | cb(err);
381 | } else {
382 | User.findById(id, function (err, user) {
383 | if (err) {
384 | cb(err);
385 | }
386 | cb(null, user);
387 | });
388 | }
389 | });
390 | }
391 | });
392 | };
393 | User.list = function (req, cb) {
394 | let {query, filter} = req.query;
395 | const {skip, limit} = filter || {skip: 0, limit: 10};
396 | const {body, params} = req;
397 | console.log({body, params, query, filter});
398 | query = query && query !== 'undefined' ? new RegExp(`^${query}`, 'i') : false;
399 | const where = query ? {
400 | and: [{status: true}, {
401 | or: [{name: query}
402 | , {surname: query}
403 | , {username: query}
404 | , {email: query}]
405 | }]
406 | } : {status: true};
407 | console.log({query});
408 | User.count(where,
409 | (err, count) => {
410 | User.find({
411 | where: where,
412 | //fields: {id: true, name: true, username: true, createdAt: true, status: true},
413 | order: 'name ASC',
414 | skip, limit
415 | },
416 | (err, items) => {
417 | if (err) return cb(err);
418 | console.log('Users', items.length);
419 | cb(null, {items, count});
420 | });
421 | });
422 | };
423 | };
424 |
--------------------------------------------------------------------------------
/common/models/user.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "user",
3 | "base": "User",
4 | "idInjection": true,
5 | "options": {
6 | "validateUpsert": true,
7 | "strictObjectIDCoercion": true
8 | },
9 | "scope": {
10 | "include": [
11 | "roles"
12 | ]
13 | },
14 | "mixins": {
15 | "FullName": true,
16 | "TimeStamp": true
17 | },
18 | "restrictResetPasswordTokenScope": true,
19 | "emailVerificationRequired": true,
20 | "properties": {
21 | "username": {
22 | "type": "string",
23 | "required": "true",
24 | "index": {
25 | "unique": true
26 | }
27 | },
28 | "name": {
29 | "type": "string",
30 | "required": "true"
31 | },
32 | "surname": {
33 | "type": "string",
34 | "required": "true"
35 | },
36 | "adminVerified": {
37 | "type": "boolean",
38 | "default": false
39 | },
40 | "status": {
41 | "type": "boolean",
42 | "default": true
43 | }
44 | },
45 | "validations": [
46 | {
47 | "username": {
48 | "type": "string",
49 | "description": "User account name",
50 | "min": 5,
51 | "max": 22
52 | },
53 | "facetName": "common"
54 | }
55 | ],
56 | "relations": {
57 | "roles": {
58 | "type": "hasMany",
59 | "model": "Role",
60 | "foreignKey": "principalId",
61 | "through": "RoleMapping"
62 | },
63 | "salesSlips": {
64 | "type": "hasMany",
65 | "model": "salesSlip",
66 | "foreignKey": "ownerId",
67 | "options": {
68 | "nestRemoting": true,
69 | "disableInclude": true
70 | },
71 | "through": "salesSlip"
72 | },
73 | "Lexicon": {
74 | "type": "hasMany",
75 | "model": "Lexicon",
76 | "foreignKey": "createdBy",
77 | "options": {
78 | "nestRemoting": true,
79 | "disableInclude": true
80 | },
81 | "through": "Lexicon"
82 | }
83 | },
84 | "acls": [
85 | {
86 | "accessType": "*",
87 | "principalType": "ROLE",
88 | "principalId": "$everyone",
89 | "permission": "DENY",
90 | "property": "*"
91 | },
92 | {
93 | "accessType": "READ",
94 | "principalType": "ROLE",
95 | "principalId": "$authenticated",
96 | "permission": "ALLOW"
97 | },
98 | {
99 | "accessType": "EXECUTE",
100 | "principalType": "ROLE",
101 | "principalId": "$owner",
102 | "permission": "ALLOW",
103 | "property": [
104 | "image",
105 | "cover"
106 | ]
107 | },
108 | {
109 | "accessType": "EXECUTE",
110 | "principalType": "ROLE",
111 | "principalId": "admin",
112 | "permission": "ALLOW",
113 | "property": [
114 | "toggleAdmin",
115 | "toggleEditor",
116 | "toggleManager",
117 | "toggleWorker",
118 | "toggleStatus"
119 | ]
120 | },
121 | {
122 | "accessType": "*",
123 | "principalType": "ROLE",
124 | "principalId": "admin",
125 | "permission": "ALLOW",
126 | "property": "*"
127 | },
128 | {
129 | "accessType": "*",
130 | "principalType": "ROLE",
131 | "principalId": "editor",
132 | "permission": "ALLOW",
133 | "property": [
134 | "toggleEditor",
135 | "toggleManager",
136 | "toggleWorker",
137 | "toggleStatus",
138 | "find",
139 | "findById",
140 | "findOne",
141 | "updateAttributes",
142 | "updateAll",
143 | "upsert"
144 | ]
145 | },
146 | {
147 | "accessType": "WRITE",
148 | "principalType": "ROLE",
149 | "principalId": "editor",
150 | "permission": "ALLOW",
151 | "property": "*"
152 | }
153 | ],
154 | "methods": {
155 | "list": {
156 | "accepts": [
157 | {
158 | "arg": "req",
159 | "type": "object",
160 | "http": {
161 | "source": "req"
162 | }
163 | }
164 | ],
165 | "returns": {
166 | "arg": "list",
167 | "type": "array"
168 | },
169 | "http": {
170 | "path": "/list",
171 | "verb": "get"
172 | }
173 | },
174 | "cover": {
175 | "accepts": [
176 | {
177 | "arg": "id",
178 | "type": "string"
179 | },
180 | {
181 | "arg": "context",
182 | "type": "object",
183 | "http": {
184 | "source": "context"
185 | }
186 | },
187 | {
188 | "arg": "options",
189 | "type": "object",
190 | "http": "optionsFromRequest"
191 | }
192 | ],
193 | "returns": {
194 | "arg": "user",
195 | "type": "object"
196 | },
197 | "http": {
198 | "path": "/:id/cover",
199 | "verb": "post"
200 | }
201 | },
202 | "image": {
203 | "accepts": [
204 | {
205 | "arg": "id",
206 | "type": "string"
207 | },
208 | {
209 | "arg": "context",
210 | "type": "object",
211 | "http": {
212 | "source": "context"
213 | }
214 | },
215 | {
216 | "arg": "options",
217 | "type": "object",
218 | "http": "optionsFromRequest"
219 | }
220 | ],
221 | "returns": {
222 | "arg": "user",
223 | "type": "object"
224 | },
225 | "http": {
226 | "path": "/:id/image",
227 | "verb": "post"
228 | }
229 | },
230 | "approve": {
231 | "accepts": [
232 | {
233 | "arg": "id",
234 | "type": "string"
235 | }
236 | ],
237 | "returns": {
238 | "arg": "user",
239 | "type": "object"
240 | },
241 | "http": {
242 | "path": "/:id/approve",
243 | "verb": "post"
244 | }
245 | },
246 | "profile": {
247 | "accepts": [
248 | {
249 | "arg": "username",
250 | "type": "string"
251 | }
252 | ],
253 | "returns": {
254 | "arg": "user",
255 | "type": "object"
256 | },
257 | "http": {
258 | "path": "/profile/:username",
259 | "verb": "get"
260 | }
261 | },
262 | "toggleAdmin": {
263 | "accepts": [
264 | {
265 | "arg": "id",
266 | "type": "string"
267 | }
268 | ],
269 | "returns": {
270 | "arg": "data",
271 | "type": "object"
272 | },
273 | "http": {
274 | "path": "/:id/toggleAdmin",
275 | "verb": "post"
276 | }
277 | },
278 | "toggleEditor": {
279 | "accepts": [
280 | {
281 | "arg": "id",
282 | "type": "string"
283 | }
284 | ],
285 | "returns": {
286 | "arg": "data",
287 | "type": "object"
288 | },
289 | "http": {
290 | "path": "/:id/toggleEditor",
291 | "verb": "post"
292 | }
293 | },
294 | "toggleManager": {
295 | "accepts": [
296 | {
297 | "arg": "id",
298 | "type": "string"
299 | }
300 | ],
301 | "returns": {
302 | "arg": "data",
303 | "type": "object"
304 | },
305 | "http": {
306 | "path": "/:id/toggleManager",
307 | "verb": "post"
308 | }
309 | },
310 | "toggleWorker": {
311 | "accepts": [
312 | {
313 | "arg": "id",
314 | "type": "string"
315 | }
316 | ],
317 | "returns": {
318 | "arg": "data",
319 | "type": "object"
320 | },
321 | "http": {
322 | "path": "/:id/toggleWorker",
323 | "verb": "post"
324 | }
325 | },
326 | "toggleStatus": {
327 | "accepts": [
328 | {
329 | "arg": "id",
330 | "type": "string"
331 | }
332 | ],
333 | "returns": {
334 | "arg": "data",
335 | "type": "object"
336 | },
337 | "http": {
338 | "path": "/:id/toggleStatus",
339 | "verb": "post"
340 | }
341 | }
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/data/README.md:
--------------------------------------------------------------------------------
1 | file storage
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "loopback-react",
3 | "version": "1.0.0",
4 | "main": "server/server.js",
5 | "engines": {
6 | "node": ">=4"
7 | },
8 | "scripts": {
9 | "lint": "eslint .",
10 | "start": "node .",
11 | "watch": "nohup nodemon ./server/server.js {
2 | //commit
3 | server.enableAuth();
4 | };
5 |
--------------------------------------------------------------------------------
/server/boot/init.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = (app) => {
4 | if (!app.get('initialData')) return;
5 |
6 | const User = app.models.user;
7 | const Role = app.models.Role;
8 | const RoleMapping = app.models.RoleMapping;
9 | // create a user
10 | User.findOrCreate({where: {email: 'admin@goker.me'}},
11 | {
12 | name: 'Admin',
13 | surname: '-',
14 | username: 'admin',
15 | email: 'admin@goker.me',
16 | password: 'admin',
17 | emailVerified: true
18 | }
19 | , function (err, user) {
20 | if (err) console.log('ERROR', err);
21 | console.log('Created user:', user);
22 |
23 | //create the admin role
24 | Role.findOrCreate({where: {name: 'admin'}}, {
25 | name: 'admin'
26 | }, function (err, role) {
27 | if (err) console.log('ERROR', err);
28 |
29 | // make an admin user
30 | RoleMapping.findOrCreate({
31 | where: {
32 | principalId: user.id,
33 | roleId: role.id
34 | }
35 | }, {
36 | principalType: RoleMapping.USER,
37 | principalId: user.id,
38 | roleId: role.id
39 | }, function (err, principal) {
40 | if (err) console.log('ERROR', err);
41 | console.log('Created principal:', principal);
42 | });
43 | });
44 | });
45 |
46 | User.findOrCreate({where: {email: 'editor@goker.me'}},
47 | {
48 | name: 'Editor',
49 | surname: '-',
50 | username: 'editor',
51 | email: 'editor@goker.me',
52 | password: 'editor',
53 | emailVerified: true
54 | }
55 | , function (err, user) {
56 | if (err) console.log('ERROR', err);
57 | console.log('Created user:', user);
58 |
59 | //create the editor role
60 | Role.findOrCreate({where: {name: 'editor'}}, {
61 | name: 'editor'
62 | }, function (err, role) {
63 | if (err) console.log('ERROR', err);
64 |
65 | // make an editor user
66 | RoleMapping.findOrCreate({
67 | where: {
68 | principalId: user.id,
69 | roleId: role.id
70 | }
71 | }, {
72 | principalType: RoleMapping.USER,
73 | principalId: user.id,
74 | roleId: role.id
75 | }, function (err, principal) {
76 | if (err) console.log('ERROR', err);
77 | console.log('Created principal:', principal);
78 | });
79 | });
80 | });
81 |
82 | User.findOrCreate({where: {email: 'manager@goker.me'}},
83 | {
84 | name: 'Manager',
85 | surname: '-',
86 | username: 'manager',
87 | email: 'manager@goker.me',
88 | password: 'manager',
89 | emailVerified: true
90 | }
91 | , function (err, user) {
92 | if (err) console.log('ERROR', err);
93 | console.log('Created user:', user);
94 |
95 | //create the editor role
96 | Role.findOrCreate({where: {name: 'manager'}}, {
97 | name: 'manager'
98 | }, function (err, role) {
99 | if (err) console.log('ERROR', err);
100 |
101 | // make an editor user
102 | RoleMapping.findOrCreate({
103 | where: {
104 | principalId: user.id,
105 | roleId: role.id
106 | }
107 | }, {
108 | principalType: RoleMapping.USER,
109 | principalId: user.id,
110 | roleId: role.id
111 | }, function (err, principal) {
112 | if (err) console.log('ERROR', err);
113 | console.log('Created principal:', principal);
114 | });
115 | });
116 | });
117 |
118 | User.findOrCreate({where: {email: 'worker@goker.me'}},
119 | {
120 | name: 'Worker',
121 | surname: '-',
122 | username: 'worker',
123 | email: 'worker@goker.me',
124 | password: 'worker',
125 | emailVerified: true
126 | }
127 | , function (err, user) {
128 | if (err) console.log('ERROR', err);
129 | console.log('Created user:', user);
130 |
131 | //create the editor role
132 | Role.findOrCreate({where: {name: 'worker'}}, {
133 | name: 'worker'
134 | }, function (err, role) {
135 | if (err) console.log('ERROR', err);
136 |
137 | // make an editor user
138 | RoleMapping.findOrCreate({
139 | where: {
140 | principalId: user.id,
141 | roleId: role.id
142 | }
143 | }, {
144 | principalType: RoleMapping.USER,
145 | principalId: user.id,
146 | roleId: role.id
147 | }, function (err, principal) {
148 | if (err) console.log('ERROR', err);
149 | console.log('Created principal:', principal);
150 | });
151 | });
152 | });
153 |
154 |
155 | User.findOrCreate({where: {email: 'user@goker.me'}},
156 | {
157 | name: 'User',
158 | surname: '-',
159 | username: 'user',
160 | email: 'user@goker.me',
161 | password: 'user',
162 | emailVerified: true
163 | }
164 | , function (err, user) {
165 | if (err) console.log('ERROR', err);
166 | console.log('Created user:', user);
167 | });
168 |
169 | };
170 |
--------------------------------------------------------------------------------
/server/boot/root.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = server => {
3 | // Install a `/status` route that returns server status
4 | const router = server.loopback.Router();
5 | router.get('/status', server.loopback.status());
6 | server.use(router);
7 | };
8 |
--------------------------------------------------------------------------------
/server/component-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "loopback-component-explorer": {
3 | "mountPath": "/explorer",
4 | "generateOperationScopedModels": true
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/server/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "loopback-react",
3 | "url": "http://localhost:3003",
4 | "fromMail": "info@localhost",
5 | "restApiRoot": "/api",
6 | "initialData": true,
7 | "host": "0.0.0.0",
8 | "port": 3003,
9 | "mandrillApiKey": "",
10 | "remoting": {
11 | "context": false,
12 | "rest": {
13 | "handleErrors": false,
14 | "normalizeHttpPath": false,
15 | "xml": false
16 | },
17 | "json": {
18 | "strict": false,
19 | "limit": "100kb"
20 | },
21 | "urlencoded": {
22 | "extended": true,
23 | "limit": "100kb"
24 | },
25 | "cors": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/server/datasources.json:
--------------------------------------------------------------------------------
1 | {
2 | "memory": {
3 | "name": "memory",
4 | "connector": "memory"
5 | },
6 | "mongo": {
7 | "name": "mongo",
8 | "url": "mongodb://127.0.0.1:27017/loopback-react",
9 | "password": "",
10 | "user": "",
11 | "useNewUrlParser": "true",
12 | "connector": "mongodb"
13 | },
14 | "storage": {
15 | "name": "storage",
16 | "connector": "loopback-component-storage",
17 | "provider": "filesystem",
18 | "root": "./data/",
19 | "maxFileSize": "52428800",
20 | "nameConflict": "makeUnique"
21 | },
22 | "email": {
23 | "name": "email",
24 | "connector": "mail",
25 | "transports": [
26 | {
27 | "type": "smtp",
28 | "host": "smtp.yandex.com",
29 | "secure": true,
30 | "port": 465,
31 | "tls": {
32 | "rejectUnauthorized": false
33 | },
34 | "auth": {
35 | "user": "USER",
36 | "pass": "PASS"
37 | }
38 | }
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/server/middleware.development.json:
--------------------------------------------------------------------------------
1 | {
2 | "final:after": {
3 | "strong-error-handler": {
4 | "params": {
5 | "debug": true,
6 | "log": true
7 | }
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/server/middleware.json:
--------------------------------------------------------------------------------
1 | {
2 | "initial:before": {
3 | "loopback#favicon": {}
4 | },
5 | "initial": {
6 | "compression": {},
7 | "cors": {
8 | "params": {
9 | "origin": true,
10 | "credentials": true,
11 | "maxAge": 86400
12 | }
13 | },
14 | "helmet#xssFilter": {},
15 | "helmet#frameguard": {
16 | "params": [
17 | "deny"
18 | ]
19 | },
20 | "helmet#hsts": {
21 | "params": {
22 | "maxAge": 0,
23 | "includeSubdomains": true
24 | }
25 | },
26 | "helmet#hidePoweredBy": {},
27 | "helmet#ieNoOpen": {},
28 | "helmet#noSniff": {},
29 | "helmet#noCache": {
30 | "enabled": false
31 | }
32 | },
33 | "session": {},
34 | "auth": {},
35 | "parse": {},
36 | "routes": {
37 | "loopback#rest": {
38 | "paths": [
39 | "${restApiRoot}"
40 | ]
41 | }
42 | },
43 | "files": {
44 | "loopback#static": [
45 | {
46 | "paths": ["/"],
47 | "params": "$!../client/build"
48 | },
49 | {
50 | "paths": ["*"],
51 | "params": "$!../client/build"
52 | }
53 | ]
54 | },
55 | "final": {
56 | "loopback#urlNotFound": {}
57 | },
58 | "final:after": {
59 | "strong-error-handler": {
60 | "params": {
61 | "debug": true,
62 | "log": true
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/server/model-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "sources": [
4 | "loopback/common/models",
5 | "loopback/server/models",
6 | "../common/models",
7 | "./models"
8 | ],
9 | "mixins": [
10 | "loopback/common/mixins",
11 | "loopback/server/mixins",
12 | "../common/mixins",
13 | "./mixins"
14 | ]
15 | },
16 | "User": {
17 | "dataSource": "mongo",
18 | "public": false
19 | },
20 | "AccessToken": {
21 | "dataSource": "mongo",
22 | "public": true
23 | },
24 | "UserCredential": {
25 | "dataSource": "mongo",
26 | "public": false
27 | },
28 | "UserIdentity": {
29 | "dataSource": "mongo",
30 | "public": false
31 | },
32 | "ACL": {
33 | "dataSource": "mongo",
34 | "public": false
35 | },
36 | "RoleMapping": {
37 | "dataSource": "mongo",
38 | "public": false,
39 | "options": {
40 | "strictObjectIDCoercion": true
41 | }
42 | },
43 | "Role": {
44 | "dataSource": "mongo",
45 | "public": false
46 | },
47 | "user": {
48 | "dataSource": "mongo",
49 | "public": true,
50 | "options": {
51 | "emailVerificationRequired": true
52 | }
53 | },
54 | "container": {
55 | "dataSource": "storage",
56 | "public": true
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/server/models/container.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function (Container) {
4 |
5 | Container.putContainer = function (container, cb) {
6 |
7 | Container.getContainer(container, function (err, c) {
8 | // if (err)
9 | // return cb(err)
10 | if (c && c.name) {
11 | console.log('CONTAINER ALREADY EXIST', container);
12 | cb(null, c.name)
13 | }
14 | else {
15 | Container.createContainer({name: container}, function (err, c) {
16 | if (err)
17 | return cb(err);
18 | console.log('CONTAINER CREATED', container);
19 | cb(null, c.name)
20 | });
21 | }
22 | });
23 |
24 |
25 | };
26 |
27 | };
28 |
--------------------------------------------------------------------------------
/server/models/container.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "container",
3 | "base": "Model",
4 | "idInjection": true,
5 | "options": {
6 | "validateUpsert": true
7 | },
8 | "properties": {},
9 | "validations": [],
10 | "relations": {},
11 | "acls": [],
12 | "methods": {}
13 | }
14 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var loopback = require('loopback');
4 | var boot = require('loopback-boot');
5 | var errorHandler = require('strong-error-handler');
6 |
7 | var app = module.exports = loopback();
8 | app.use(loopback.token({
9 | model: app.models.accessToken,
10 | currentUserLiteral: 'me'
11 | }));
12 |
13 | app.use(errorHandler({
14 | debug: true,
15 | log: true,
16 | }));
17 |
18 | // Passport configurators..
19 | var loopbackPassport = require('loopback-component-passport');
20 | var PassportConfigurator = loopbackPassport.PassportConfigurator;
21 | var passportConfigurator = new PassportConfigurator(app);
22 |
23 | app.start = function () {
24 | // start the web server
25 | return app.listen(function () {
26 | app.emit('started');
27 | var baseUrl = app.get('url').replace(/\/$/, '');
28 | console.log('Web server listening at: %s', baseUrl);
29 | if (app.get('loopback-component-explorer')) {
30 | var explorerPath = app.get('loopback-component-explorer').mountPath;
31 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
32 | }
33 | });
34 | };
35 |
36 | // Bootstrap the application, configure models, datasources and middleware.
37 | // Sub-apps like REST API are mounted via boot scripts.
38 | boot(app, __dirname, function (err) {
39 | if (err) console.log(err);//throw err;
40 |
41 | // start the server if `$ node server.js`
42 | if (require.main === module)
43 | app.start();
44 | });
45 |
--------------------------------------------------------------------------------